use sea_orm::{entity::prelude::*, QuerySelect, SelectColumns};

use apb::{ActorMut, ActorType, BaseMut, DocumentMut, EndpointsMut, ObjectMut, PublicKeyMut};

use crate::ext::{JsonVec, TypeName};

#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Field {
	#[serde(default)]
	pub name: String,
	#[serde(default)]
	pub value: String,
	#[serde(default, skip_serializing_if = "Option::is_none")]
	pub verified_at: Option<ChronoDateTimeUtc>,
	#[serde(default, rename = "type")]
	pub field_type: String,
}

impl TypeName for Field {
	fn type_name() -> String {
		"Field".to_string()
	}
}

impl<T: apb::Object> From<T> for Field {
	fn from(value: T) -> Self {
		Field {
			name: value.name().unwrap_or_default().to_string(),
			value: mdhtml::safe_html(&value.value().unwrap_or_default()),
			field_type: "PropertyValue".to_string(), // TODO can we try parsing this instead??
			verified_at: None, // TODO where does verified_at come from? extend apb maybe
		}
	}
}

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "actors")]
pub struct Model {
	#[sea_orm(primary_key)]
	pub internal: i64,
	#[sea_orm(unique)]
	pub id: String,
	pub actor_type: ActorType,
	pub domain: String,
	pub name: Option<String>,
	pub summary: Option<String>,
	pub image: Option<String>,
	pub icon: Option<String>,
	pub preferred_username: String,
	pub fields: JsonVec<Field>,
	pub inbox: Option<String>,
	pub shared_inbox: Option<String>,
	pub outbox: Option<String>,
	pub following: Option<String>,
	pub followers: Option<String>,
	pub following_count: i32,
	pub followers_count: i32,
	pub statuses_count: i32,
	pub public_key: String,
	pub private_key: Option<String>,
	pub published: ChronoDateTimeUtc,
	pub updated: ChronoDateTimeUtc,
	pub also_known_as: JsonVec<String>,
	pub moved_to: Option<String>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
	#[sea_orm(has_many = "super::activity::Entity")]
	Activities,
	#[sea_orm(has_many = "super::addressing::Entity")]
	Addressing,
	#[sea_orm(has_many = "super::announce::Entity")]
	Announces,
	#[sea_orm(has_many = "super::config::Entity")]
	Configs,
	#[sea_orm(has_many = "super::credential::Entity")]
	Credentials,
	#[sea_orm(
		belongs_to = "super::instance::Entity",
		from = "Column::Domain",
		to = "super::instance::Column::Domain",
		on_update = "Cascade",
		on_delete = "NoAction"
	)]
	Instances,
	#[sea_orm(has_many = "super::dislike::Entity")]
	Dislikes,
	#[sea_orm(has_many = "super::like::Entity")]
	Likes,
	#[sea_orm(has_many = "super::mention::Entity")]
	Mentions,
	#[sea_orm(has_many = "super::notification::Entity")]
	Notifications,
	#[sea_orm(has_many = "super::object::Entity")]
	Objects,
	#[sea_orm(has_many = "super::relation::Entity")]
	Relations,
	#[sea_orm(has_many = "super::session::Entity")]
	Sessions,
}

impl Related<super::activity::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Activities.def()
	}
}

impl Related<super::addressing::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Addressing.def()
	}
}

impl Related<super::announce::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Announces.def()
	}
}

impl Related<super::config::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Configs.def()
	}
}

impl Related<super::credential::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Credentials.def()
	}
}

impl Related<super::instance::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Instances.def()
	}
}

impl Related<super::dislike::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Dislikes.def()
	}
}

impl Related<super::like::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Likes.def()
	}
}

impl Related<super::mention::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Mentions.def()
	}
}

impl Related<super::notification::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Notifications.def()
	}
}

impl Related<super::object::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Objects.def()
	}
}

impl Related<super::relation::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Relations.def()
	}
}

impl Related<super::session::Entity> for Entity {
	fn to() -> RelationDef {
		Relation::Sessions.def()
	}
}

impl ActiveModelBehavior for ActiveModel {}

impl Entity {
	pub fn find_by_ap_id(id: &str) -> Select<Entity> {
		Entity::find().filter(Column::Id.eq(id))
	}

	pub fn delete_by_ap_id(id: &str) -> sea_orm::DeleteMany<Entity> {
		Entity::delete_many().filter(Column::Id.eq(id))
	}

	pub async fn ap_to_internal(id: &str, db: &impl ConnectionTrait) -> Result<Option<i64>, DbErr> {
		Entity::find()
			.filter(Column::Id.eq(id))
			.select_only()
			.select_column(Column::Internal)
			.into_tuple::<i64>()
			.one(db)
			.await
	}
}

impl crate::ext::IntoActivityPub for Model {
	fn into_activity_pub_json(self, ctx: &crate::Context) -> serde_json::Value {
		let is_local = ctx.is_local(&self.id);
		let id = ctx.id(&self.id);
		apb::new()
			.set_id(Some(self.id.clone()))
			.set_actor_type(Some(self.actor_type))
			.set_name(self.name)
			.set_summary(self.summary)
			.set_icon(apb::Node::maybe_object(self.icon.map(|i|
				apb::new()
					.set_document_type(Some(apb::DocumentType::Image))
					.set_url(apb::Node::link(i.clone()))
			)))
			.set_image(apb::Node::maybe_object(self.image.map(|i|
				apb::new()
					.set_document_type(Some(apb::DocumentType::Image))
					.set_url(apb::Node::link(i.clone()))
			)))
			.set_attachment(apb::Node::array(
				self.fields.0
					.into_iter()
					.filter_map(|x| serde_json::to_value(x).ok())
					.collect()
			))
			.set_published(Some(self.published))
			.set_updated(if self.updated != self.published { Some(self.updated) } else { None })
			.set_preferred_username(Some(self.preferred_username))
			.set_statuses_count(Some(self.statuses_count as u64))
			// local users may want to hide these! default to hidden, and downstream we can opt-in to
			// showing them. for remote users we assume the number is already "protected" by remote
			// instance so we just show it
			.set_followers_count(if is_local { None } else { Some(self.followers_count as u64) })
			.set_following_count(if is_local { None } else { Some(self.following_count as u64) })
			.set_inbox(if is_local { apb::Node::link(crate::url!(ctx, "/actors/{id}/inbox")) } else { apb::Node::maybe_link(self.inbox) })
			.set_outbox(if is_local { apb::Node::link(crate::url!(ctx, "/actors/{id}/outbox")) } else { apb::Node::maybe_link(self.outbox) })
			.set_following(if is_local { apb::Node::link(crate::url!(ctx, "/actors{id}/following")) } else { apb::Node::maybe_link(self.following) })
			.set_followers(if is_local { apb::Node::link(crate::url!(ctx, "/actors/{id}/followers")) } else { apb::Node::maybe_link(self.followers) })
			.set_public_key(apb::Node::object(
				apb::new()
					.set_id(Some(format!("{}#main-key", self.id)))
					.set_owner(Some(self.id))
					.set_public_key_pem(self.public_key)
			))
			.set_endpoints(apb::Node::object(
				apb::new()
					.set_shared_inbox(if is_local { Some(crate::url!(ctx, "/inbox")) } else { self.shared_inbox })
					.set_proxy_url(if is_local { Some(crate::url!(ctx, "/fetch")) } else { None })
			))
			.set_also_known_as(apb::Node::links(self.also_known_as.0))
			.set_moved_to(apb::Node::maybe_link(self.moved_to))
			.set_discoverable(Some(true))
	}
}