use crate::extract::{Extractor, InternalAuth, ParamFromStr};
use crate::RouterApi;
use axum::body::Body;
use axum::extract::{Path, Query};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use etwin_core::auth::AuthContext;
use etwin_core::core::LocaleId;
use etwin_core::forum::{
  AddModeratorOptions, CreatePostOptions, CreateThreadOptions, DeleteModeratorOptions, ForumPost, ForumPostId,
  ForumPostRevisionComment, ForumPostRevisionId, ForumSection, ForumSectionDisplayName, ForumSectionKey,
  ForumSectionListing, ForumSectionRef, ForumThread, ForumThreadRef, ForumThreadTitle, GetForumSectionOptions,
  GetThreadOptions, MarktwinText, UpdatePostOptions, UpsertSystemSectionOptions,
};
use etwin_core::types::AnyError;
use etwin_core::user::UserIdRef;
use etwin_services::forum::GetForumPostOptions;
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;

pub fn router() -> Router<Body> {
  Router::new()
    .route("/sections", get(get_sections).put(upsert_system_section))
    .route("/sections/:section_ref", get(get_section).post(create_thread))
    .route(
      "/sections/:section_ref/role_grants",
      post(add_moderator).delete(remove_moderator),
    )
    .route("/threads/:thread_ref", get(get_thread).post(create_post))
    .route("/posts/:post_ref", get(get_post).patch(update_post))
}
use etwin_serde_tools::{deserialize_explicit_option, deserialize_nested_option};

#[derive(Debug, Error)]
enum GetSectionsError {
  #[error("internal error")]
  Internal(#[from] etwin_services::forum::GetSectionsError),
}

impl IntoResponse for GetSectionsError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn get_sections(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
) -> Result<Json<ForumSectionListing>, GetSectionsError> {
  Ok(Json(api.forum.get_sections(&acx.value()).await?))
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
struct UpsertSystemSectionBody {
  pub key: ForumSectionKey,
  pub display_name: ForumSectionDisplayName,
  pub locale: Option<LocaleId>,
}

#[derive(Debug, Error)]
enum UpsertSystemSectionError {
  #[error("internal error")]
  Internal,
}

impl From<etwin_core::forum::UpsertSystemSectionError> for UpsertSystemSectionError {
  fn from(inner: etwin_core::forum::UpsertSystemSectionError) -> Self {
    use etwin_core::forum::UpsertSystemSectionError::*;
    match inner {
      Other(..) => Self::Internal,
    }
  }
}

impl IntoResponse for UpsertSystemSectionError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn upsert_system_section(
  Extension(api): Extension<RouterApi>,
  _auth: Extractor<InternalAuth>,
  Json(body): Json<UpsertSystemSectionBody>,
) -> Result<Json<ForumSection>, UpsertSystemSectionError> {
  Ok(Json(
    api
      .forum
      .upsert_system_section(&UpsertSystemSectionOptions {
        key: body.key,
        display_name: body.display_name,
        locale: body.locale,
      })
      .await?,
  ))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct GetSectionQuery {
  offset: Option<u32>,
  limit: Option<u32>,
}

#[derive(Debug, Error)]
enum GetSectionError {
  #[error("section not found")]
  SectionNotFound,
  #[error("internal error")]
  Internal,
}

impl From<etwin_services::forum::GetSectionError> for GetSectionError {
  fn from(inner: etwin_services::forum::GetSectionError) -> Self {
    use etwin_services::forum::GetSectionError::*;
    match inner {
      SectionNotFound => Self::SectionNotFound,
      Internal(..) => Self::Internal,
    }
  }
}

impl IntoResponse for GetSectionError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::SectionNotFound => StatusCode::NOT_FOUND,
      Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn get_section(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(section_ref)): Path<ParamFromStr<ForumSectionRef>>,
) -> Result<Json<ForumSection>, GetSectionError> {
  let config = api.forum.config();
  Ok(Json(
    api
      .forum
      .get_section(
        &acx.value(),
        &GetForumSectionOptions {
          section: section_ref,
          thread_offset: 0,
          thread_limit: config.threads_per_page,
        },
      )
      .await?,
  ))
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
struct CreateThreadBody {
  title: ForumThreadTitle,
  body: MarktwinText,
}

#[derive(Debug, Error)]
enum CreateThreadError {
  #[error("section not found")]
  SectionNotFound,
  #[error("current actor does not have the permission to create a thread in this section")]
  Forbidden,
  #[error("failed to parse provided body")]
  FailedToParseBody,
  #[error("failed to render provided body")]
  FailedToRenderBody,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::CreateThreadError> for CreateThreadError {
  fn from(inner: etwin_services::forum::CreateThreadError) -> Self {
    use etwin_services::forum::CreateThreadError::*;
    match inner {
      SectionNotFound => Self::SectionNotFound,
      Forbidden => Self::Forbidden,
      FailedToParseBody => Self::FailedToParseBody,
      FailedToRenderBody => Self::FailedToRenderBody,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for CreateThreadError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::SectionNotFound => StatusCode::NOT_FOUND,
      Self::Forbidden => StatusCode::FORBIDDEN,
      Self::FailedToParseBody | Self::FailedToRenderBody => StatusCode::BAD_REQUEST,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn create_thread(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(section_ref)): Path<ParamFromStr<ForumSectionRef>>,
  Json(body): Json<CreateThreadBody>,
) -> Result<Json<ForumThread>, CreateThreadError> {
  Ok(Json(
    api
      .forum
      .create_thread(
        &acx.value(),
        &CreateThreadOptions {
          section: section_ref,
          title: body.title,
          body: body.body,
        },
      )
      .await?,
  ))
}

#[derive(Debug, Error)]
enum AddModeratorError {
  #[error("section not found")]
  SectionNotFound,
  #[error("target grantee user not found")]
  TargetUserNotFound,
  #[error("current actor does not have the permission to add moderators to this section")]
  Forbidden,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::AddModeratorError> for AddModeratorError {
  fn from(inner: etwin_services::forum::AddModeratorError) -> Self {
    use etwin_services::forum::AddModeratorError::*;
    match inner {
      SectionNotFound => Self::SectionNotFound,
      TargetUserNotFound => Self::TargetUserNotFound,
      Forbidden => Self::Forbidden,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for AddModeratorError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::SectionNotFound => StatusCode::NOT_FOUND,
      Self::Forbidden => StatusCode::FORBIDDEN,
      Self::TargetUserNotFound => StatusCode::UNPROCESSABLE_ENTITY,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn add_moderator(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(section_ref)): Path<ParamFromStr<ForumSectionRef>>,
  Json(body): Json<UserIdRef>,
) -> Result<Json<ForumSection>, AddModeratorError> {
  Ok(Json(
    api
      .forum
      .add_moderator(
        &acx.value(),
        &AddModeratorOptions {
          section: section_ref,
          user: body.id.into(),
        },
      )
      .await?,
  ))
}

#[derive(Debug, Error)]
enum RemoveModeratorError {
  #[error("section not found")]
  SectionNotFound,
  #[error("target grantee user not found")]
  TargetUserNotFound,
  #[error("current actor does not have the permission to remove moderator")]
  Forbidden,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::DeleteModeratorError> for RemoveModeratorError {
  fn from(inner: etwin_services::forum::DeleteModeratorError) -> Self {
    use etwin_services::forum::DeleteModeratorError::*;
    match inner {
      SectionNotFound => Self::SectionNotFound,
      TargetUserNotFound => Self::TargetUserNotFound,
      Forbidden => Self::Forbidden,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for RemoveModeratorError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::SectionNotFound => StatusCode::NOT_FOUND,
      Self::Forbidden => StatusCode::FORBIDDEN,
      Self::TargetUserNotFound => StatusCode::UNPROCESSABLE_ENTITY,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn remove_moderator(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(section_ref)): Path<ParamFromStr<ForumSectionRef>>,
  Json(body): Json<UserIdRef>,
) -> Result<Json<ForumSection>, RemoveModeratorError> {
  Ok(Json(
    api
      .forum
      .delete_moderator(
        &acx.value(),
        &DeleteModeratorOptions {
          section: section_ref,
          user: body.id.into(),
        },
      )
      .await?,
  ))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct GetThreadQuery {
  offset: Option<u32>,
  limit: Option<u32>,
}

#[derive(Debug, Error)]
enum GetThreadError {
  #[error("thread not found")]
  ThreadNotFound,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::GetThreadError> for GetThreadError {
  fn from(inner: etwin_services::forum::GetThreadError) -> Self {
    use etwin_services::forum::GetThreadError::*;
    match inner {
      ThreadNotFound => Self::ThreadNotFound,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for GetThreadError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::ThreadNotFound => StatusCode::NOT_FOUND,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn get_thread(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(thread_ref)): Path<ParamFromStr<ForumThreadRef>>,
  Query(query): Query<GetThreadQuery>,
) -> Result<Json<ForumThread>, GetThreadError> {
  let config = api.forum.config();
  Ok(Json(
    api
      .forum
      .get_thread(
        &acx.value(),
        &GetThreadOptions {
          thread: thread_ref,
          post_offset: query.offset.unwrap_or(0),
          post_limit: query.limit.unwrap_or(config.posts_per_page),
        },
      )
      .await?,
  ))
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
struct CreatePostBody {
  body: MarktwinText,
}

#[derive(Debug, Error)]
enum CreatePostError {
  #[error("thread not found")]
  ThreadNotFound,
  #[error("current actor does not have the permission to create a post in this thread")]
  Forbidden,
  #[error("failed to parse provided body")]
  FailedToParseBody,
  #[error("failed to render provided body")]
  FailedToRenderBody,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::CreatePostError> for CreatePostError {
  fn from(inner: etwin_services::forum::CreatePostError) -> Self {
    use etwin_services::forum::CreatePostError::*;
    match inner {
      ThreadNotFound => Self::ThreadNotFound,
      Forbidden => Self::Forbidden,
      FailedToParseBody => Self::FailedToParseBody,
      FailedToRenderBody => Self::FailedToRenderBody,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for CreatePostError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::ThreadNotFound => StatusCode::NOT_FOUND,
      Self::Forbidden => StatusCode::FORBIDDEN,
      Self::FailedToParseBody | Self::FailedToRenderBody => StatusCode::BAD_REQUEST,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn create_post(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(ParamFromStr(thread_ref)): Path<ParamFromStr<ForumThreadRef>>,
  Json(body): Json<CreatePostBody>,
) -> Result<Json<ForumPost>, CreatePostError> {
  Ok(Json(
    api
      .forum
      .create_post(
        &acx.value(),
        &CreatePostOptions {
          thread: thread_ref,
          body: body.body,
        },
      )
      .await?,
  ))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
struct GetPostQuery {
  offset: Option<u32>,
  limit: Option<u32>,
}

#[derive(Debug, Error)]
enum GetPostError {
  #[error("post not found")]
  PostNotFound,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::GetPostError> for GetPostError {
  fn from(inner: etwin_services::forum::GetPostError) -> Self {
    use etwin_services::forum::GetPostError::*;
    match inner {
      PostNotFound => Self::PostNotFound,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for GetPostError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::PostNotFound => StatusCode::NOT_FOUND,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn get_post(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(post_ref): Path<ForumPostId>,
  Query(query): Query<GetThreadQuery>,
) -> Result<Json<ForumPost>, GetPostError> {
  let config = api.forum.config();
  Ok(Json(
    api
      .forum
      .get_post(
        &acx.value(),
        &GetForumPostOptions {
          post: post_ref.into(),
          revision_offset: query.offset.unwrap_or(0),
          revision_limit: query.limit.unwrap_or(config.posts_per_page),
          time: None,
        },
      )
      .await?,
  ))
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
struct UpdatePostBody {
  last_revision_id: ForumPostRevisionId,
  #[serde(default, deserialize_with = "deserialize_nested_option")]
  content: Option<Option<MarktwinText>>,
  #[serde(default, deserialize_with = "deserialize_nested_option")]
  moderation: Option<Option<MarktwinText>>,
  #[serde(deserialize_with = "deserialize_explicit_option")]
  comment: Option<ForumPostRevisionComment>,
}

#[derive(Debug, Error)]
enum UpdatePostError {
  #[error("post not found")]
  PostNotFound,
  #[error("current actor does not have the permission to update this post")]
  Forbidden,
  #[error("failed to parse provided body")]
  FailedToParseBody,
  #[error("failed to render provided body")]
  FailedToRenderBody,
  #[error("internal error")]
  Internal(#[source] AnyError),
}

impl From<etwin_services::forum::UpdatePostError> for UpdatePostError {
  fn from(inner: etwin_services::forum::UpdatePostError) -> Self {
    use etwin_services::forum::UpdatePostError::*;
    match inner {
      PostNotFound => Self::PostNotFound,
      Forbidden => Self::Forbidden,
      FailedToParseBody => Self::FailedToParseBody,
      FailedToRenderBody => Self::FailedToRenderBody,
      Other(e) => Self::Internal(e),
    }
  }
}

impl IntoResponse for UpdatePostError {
  fn into_response(self) -> Response {
    let status = match &self {
      Self::PostNotFound => StatusCode::NOT_FOUND,
      Self::Forbidden => StatusCode::FORBIDDEN,
      Self::FailedToParseBody | Self::FailedToRenderBody => StatusCode::BAD_REQUEST,
      Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (status, Json(json!({ "error": self.to_string() }))).into_response()
  }
}

async fn update_post(
  Extension(api): Extension<RouterApi>,
  acx: Extractor<AuthContext>,
  Path(post_ref): Path<ForumPostId>,
  Json(body): Json<UpdatePostBody>,
) -> Result<Json<ForumPost>, UpdatePostError> {
  Ok(Json(
    api
      .forum
      .update_post(
        &acx.value(),
        &UpdatePostOptions {
          post: post_ref,
          revision: body.last_revision_id,
          content: body.content,
          moderation: body.moderation,
          comment: body.comment,
        },
      )
      .await?,
  ))
}

#[cfg(test)]
mod tests {
  use crate::app;
  use crate::extract::AuthLogger;
  use crate::test::{create_api, RouterExt};
  use axum::http::StatusCode;
  use axum::Extension;
  use etwin_log::NoopLogger;
  use std::sync::Arc;

  #[tokio::test]
  async fn test_get_section_not_found() {
    let api = create_api();
    let logger = AuthLogger(Arc::new(NoopLogger));
    let router = app(api, None).layer(Extension(logger));
    let mut client = router.client();

    {
      let req = axum::http::Request::builder()
        .method("GET")
        .uri("/api/v1/forum/sections/missing_section?offset=10&limit=4")
        .header("Content-Type", "application/json")
        .body(axum::body::Body::empty())
        .unwrap();

      let res = client.send(req).await;
      assert_eq!(res.status(), StatusCode::NOT_FOUND);
      let body = hyper::body::to_bytes(res.into_body())
        .await
        .expect("read body")
        .to_vec();

      let body: &str = std::str::from_utf8(body.as_slice()).unwrap();
      let expected = r#"{"error":"section not found"}"#;
      assert_eq!(body, expected);
    }
  }
}
