use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts}};
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
use httpsign::HttpSignature;
use upub::traits::{fetch::RequestError, Fetcher};

use crate::ApiError;

#[derive(Debug, Clone)]
pub enum Identity {
	Anonymous,
	Remote {
		user: String,
		domain: String,
		internal: i64,
	},
	Local {
		id: String,
		internal: i64,
	},
}

impl Identity {
	// TODO i hate having to do this, but if i don't include `activity.actor` column
	//      we can't see our own activities (likes, announces, follows), and if i do
	//      all queries which don't bring up activities break. so it's necessary to
	//      split these two in order to manually include the extra filter when
	//      needed

	pub fn filter_objects(&self) -> Condition {
		let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
		match self {
			Identity::Anonymous => base_cond,
			Identity::Remote { internal, .. } => base_cond.add(upub::model::addressing::Column::Instance.eq(*internal)), 
			Identity::Local { internal, id } => base_cond
				.add(upub::model::addressing::Column::Actor.eq(*internal))
				.add(upub::model::object::Column::AttributedTo.eq(id))
		}
	}

	pub fn filter_activities(&self) -> Condition {
		let base_cond = Condition::any().add(upub::model::addressing::Column::Actor.is_null());
		match self {
			Identity::Anonymous => base_cond,
			Identity::Remote { internal, .. } => base_cond.add(upub::model::addressing::Column::Instance.eq(*internal)), 
			Identity::Local { internal, id } => base_cond
				.add(upub::model::addressing::Column::Actor.eq(*internal))
				.add(upub::model::object::Column::AttributedTo.eq(id))
				.add(upub::model::activity::Column::Actor.eq(id)),
		}
	}

	pub fn my_id(&self) -> Option<i64> {
		match self {
			Identity::Local { internal, .. } => Some(*internal),
			_ => None,
		}
	}

	pub fn is(&self, uid: &str) -> bool {
		match self {
			Identity::Anonymous => false,
			Identity::Remote { .. } => false, // TODO per-actor server auth should check this
			Identity::Local { id, .. } => id.as_str() == uid
		}
	}

	#[allow(unused)]
	pub fn is_anon(&self) -> bool {
		matches!(self, Self::Anonymous)
	}

	#[allow(unused)]
	pub fn is_local(&self) -> bool {
		matches!(self, Self::Local { .. })
	}

	#[allow(unused)]
	pub fn is_remote(&self) -> bool {
		matches!(self, Self::Remote { .. })
	}
}

pub struct AuthIdentity(pub Identity);

#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthIdentity
where
	upub::Context: FromRef<S>,
	S: Send + Sync,
{
	type Rejection = ApiError;

	async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
		let ctx = upub::Context::from_ref(state);
		let mut identity = Identity::Anonymous;

		let auth_header = parts
			.headers
			.get(header::AUTHORIZATION)
			.map(|v| v.to_str().unwrap_or(""))
			.unwrap_or("");

		if auth_header.starts_with("Bearer ") {
			match upub::model::session::Entity::find()
				.filter(upub::model::session::Column::Secret.eq(auth_header.replace("Bearer ", "")))
				.filter(upub::model::session::Column::Expires.gt(chrono::Utc::now()))
				.one(ctx.db())
				.await?
			{
				None => return Err(ApiError::unauthorized()),
				Some(x) => {
					// TODO could we store both actor ap id and internal id in session? to avoid this extra
					// lookup on *every* local authed request we receive...
					let internal = upub::model::actor::Entity::ap_to_internal(&x.actor, ctx.db())
						.await?
						.ok_or_else(ApiError::internal_server_error)?;
					identity = Identity::Local { id: x.actor, internal };
				},
			}
		}

		if let Some(sig) = parts
			.headers
			.get("Signature")
			.map(|v| v.to_str().unwrap_or(""))
		{
			tracing::debug!("validating http signature '{sig}'");
			let mut http_signature = HttpSignature::parse(sig);

			// TODO assert payload's digest is equal to signature's
			//      really annoying to do here because we're streaming
			//      the request, maybe even impossible with this design?

			let user_id = http_signature.key_id
				.replace("/main-key", "") // gotosocial whyyyyy
				.split('#')
				.next().ok_or(ApiError::bad_request())?
				.to_string();

			match ctx.fetch_user(&user_id, ctx.db()).await {
				Err(RequestError::Database(x)) => return Err(RequestError::Database(x).into()),
				Err(e) => tracing::debug!("could not fetch {user_id} to verify signature: {e}"),
				Ok(user) => {
					let signature = http_signature.build_from_parts(parts);
					tracing::debug!("constructed http signature {signature:?}");
					let valid = signature.verify(&user.public_key)?;

					if !valid {
						tracing::warn!("refusing mismatching http signature");
						return Err(ApiError::unauthorized());
					}

					if ctx.cfg().reject.requests.contains(&user.domain) {
						return Err(ApiError::Status(axum::http::StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS));
					}

					if !ctx.cfg().reject.access.contains(&user.domain) {
						let internal = upub::model::instance::Entity::domain_to_internal(&user.domain, ctx.db())
							.await?
							.ok_or_else(ApiError::internal_server_error)?; // user but not their domain???
						identity = Identity::Remote { user: user.id, domain: user.domain, internal };
					}
				},
			}

		}

		Ok(AuthIdentity(identity))
	}
}