#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, QueryFilter, QueryOrder};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::net::SocketAddr;

use axum::{
    extract::{ConnectInfo, rejection::ExtensionRejection},
    http::{HeaderMap, header},
};

use crate::controllers::admin::check_auth;
use crate::models::_entities::{
    comments::{ActiveModel, Column, Entity, Model},
    posts,
};
use crate::services::{
    admin_audit,
    comment_guard::{self, CommentGuardInput},
    notifications,
};

const ARTICLE_SCOPE: &str = "article";
const PARAGRAPH_SCOPE: &str = "paragraph";

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Params {
    pub post_id: Option<Uuid>,
    pub post_slug: Option<String>,
    pub author: Option<String>,
    pub email: Option<String>,
    pub avatar: Option<String>,
    pub content: Option<String>,
    pub reply_to: Option<Uuid>,
    pub reply_to_comment_id: Option<i32>,
    pub scope: Option<String>,
    pub paragraph_key: Option<String>,
    pub paragraph_excerpt: Option<String>,
    pub approved: Option<bool>,
}

impl Params {
    fn update(&self, item: &mut ActiveModel) {
        if let Some(post_id) = self.post_id {
            item.post_id = Set(Some(post_id));
        }
        if let Some(post_slug) = &self.post_slug {
            item.post_slug = Set(Some(post_slug.clone()));
        }
        if let Some(author) = &self.author {
            item.author = Set(Some(author.clone()));
        }
        if let Some(email) = &self.email {
            item.email = Set(Some(email.clone()));
        }
        if let Some(avatar) = &self.avatar {
            item.avatar = Set(Some(avatar.clone()));
        }
        if let Some(content) = &self.content {
            item.content = Set(Some(content.clone()));
        }
        if let Some(reply_to) = self.reply_to {
            item.reply_to = Set(Some(reply_to));
        }
        if let Some(reply_to_comment_id) = self.reply_to_comment_id {
            item.reply_to_comment_id = Set(Some(reply_to_comment_id));
        }
        if let Some(scope) = &self.scope {
            item.scope = Set(scope.clone());
        }
        if let Some(paragraph_key) = &self.paragraph_key {
            item.paragraph_key = Set(Some(paragraph_key.clone()));
        }
        if let Some(paragraph_excerpt) = &self.paragraph_excerpt {
            item.paragraph_excerpt = Set(Some(paragraph_excerpt.clone()));
        }
        if let Some(approved) = self.approved {
            item.approved = Set(Some(approved));
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize)]
pub struct ListQuery {
    pub post_id: Option<String>,
    pub post_slug: Option<String>,
    pub scope: Option<String>,
    pub paragraph_key: Option<String>,
    pub approved: Option<bool>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct CreateCommentRequest {
    #[serde(default, alias = "postId")]
    pub post_id: Option<String>,
    #[serde(default, alias = "postSlug")]
    pub post_slug: Option<String>,
    #[serde(default, alias = "nickname")]
    pub author: Option<String>,
    #[serde(default)]
    pub email: Option<String>,
    #[serde(default)]
    pub avatar: Option<String>,
    #[serde(default)]
    pub content: Option<String>,
    #[serde(default, alias = "replyTo")]
    pub reply_to: Option<String>,
    #[serde(default, alias = "replyToCommentId")]
    pub reply_to_comment_id: Option<i32>,
    #[serde(default)]
    pub scope: Option<String>,
    #[serde(default, alias = "paragraphKey")]
    pub paragraph_key: Option<String>,
    #[serde(default, alias = "paragraphExcerpt")]
    pub paragraph_excerpt: Option<String>,
    #[serde(default)]
    pub approved: Option<bool>,
    #[serde(default, alias = "captchaToken")]
    pub captcha_token: Option<String>,
    #[serde(default, alias = "captchaAnswer")]
    pub captcha_answer: Option<String>,
    #[serde(default, alias = "turnstileToken")]
    pub turnstile_token: Option<String>,
    #[serde(default)]
    pub website: Option<String>,
}

#[derive(Clone, Debug, Serialize)]
pub struct ParagraphCommentSummary {
    pub paragraph_key: String,
    pub count: usize,
}

fn normalize_optional_string(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim().to_string();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

fn normalize_with_limit(value: Option<&str>, max_chars: usize) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim();
        if trimmed.is_empty() {
            return None;
        }

        Some(trimmed.chars().take(max_chars).collect::<String>())
    })
}

fn header_value<'a>(headers: &'a HeaderMap, key: header::HeaderName) -> Option<&'a str> {
    headers.get(key).and_then(|value| value.to_str().ok())
}

fn first_forwarded_ip(value: &str) -> Option<&str> {
    value
        .split(',')
        .map(str::trim)
        .find(|item| !item.is_empty())
}

fn detect_client_ip(
    headers: &HeaderMap,
    connect_info: Option<&ConnectInfo<SocketAddr>>,
) -> Option<String> {
    let forwarded = header_value(headers, header::HeaderName::from_static("x-forwarded-for"))
        .and_then(first_forwarded_ip);
    let real_ip = header_value(headers, header::HeaderName::from_static("x-real-ip"));
    let cf_connecting_ip =
        header_value(headers, header::HeaderName::from_static("cf-connecting-ip"));
    let true_client_ip = header_value(headers, header::HeaderName::from_static("true-client-ip"));
    let remote_addr = connect_info.map(|addr| addr.0.ip().to_string());

    normalize_with_limit(
        forwarded
            .or(real_ip)
            .or(cf_connecting_ip)
            .or(true_client_ip)
            .or(remote_addr.as_deref()),
        96,
    )
}

fn normalized_scope(value: Option<String>) -> Result<String> {
    match value
        .unwrap_or_else(|| ARTICLE_SCOPE.to_string())
        .trim()
        .to_lowercase()
        .as_str()
    {
        ARTICLE_SCOPE => Ok(ARTICLE_SCOPE.to_string()),
        PARAGRAPH_SCOPE => Ok(PARAGRAPH_SCOPE.to_string()),
        _ => Err(Error::BadRequest("invalid comment scope".to_string())),
    }
}

fn preview_excerpt(value: &str) -> Option<String> {
    let flattened = value.split_whitespace().collect::<Vec<_>>().join(" ");
    let excerpt = flattened.chars().take(120).collect::<String>();
    if excerpt.is_empty() {
        None
    } else {
        Some(excerpt)
    }
}

async fn load_item(ctx: &AppContext, id: i32) -> Result<Model> {
    let item = Entity::find_by_id(id).one(&ctx.db).await?;
    item.ok_or_else(|| Error::NotFound)
}

async fn resolve_post_slug(ctx: &AppContext, raw: &str) -> Result<Option<String>> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return Ok(None);
    }

    if let Ok(id) = trimmed.parse::<i32>() {
        let post = posts::Entity::find_by_id(id).one(&ctx.db).await?;
        return Ok(post.map(|item| item.slug));
    }

    Ok(Some(trimmed.to_string()))
}

#[debug_handler]
pub async fn list(
    Query(query): Query<ListQuery>,
    State(ctx): State<AppContext>,
    headers: HeaderMap,
) -> Result<Response> {
    if query.approved != Some(true) {
        check_auth(&headers)?;
    }

    let mut db_query = Entity::find().order_by_asc(Column::CreatedAt);

    let post_slug = if let Some(post_slug) = query.post_slug {
        Some(post_slug)
    } else if let Some(post_id) = query.post_id {
        resolve_post_slug(&ctx, &post_id).await?
    } else {
        None
    };

    if let Some(post_slug) = post_slug {
        db_query = db_query.filter(Column::PostSlug.eq(post_slug));
    }

    if let Some(scope) = query.scope {
        db_query = db_query.filter(Column::Scope.eq(scope.trim().to_lowercase()));
    }

    if let Some(paragraph_key) = query
        .paragraph_key
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        db_query = db_query.filter(Column::ParagraphKey.eq(paragraph_key));
    }

    if let Some(approved) = query.approved {
        db_query = db_query.filter(Column::Approved.eq(approved));
    }

    format::json(db_query.all(&ctx.db).await?)
}

#[debug_handler]
pub async fn paragraph_summary(
    Query(query): Query<ListQuery>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let post_slug = if let Some(post_slug) = query.post_slug {
        Some(post_slug)
    } else if let Some(post_id) = query.post_id {
        resolve_post_slug(&ctx, &post_id).await?
    } else {
        None
    }
    .ok_or_else(|| Error::BadRequest("post_slug is required".to_string()))?;

    let items = Entity::find()
        .filter(Column::PostSlug.eq(post_slug))
        .filter(Column::Scope.eq(PARAGRAPH_SCOPE))
        .filter(Column::Approved.eq(true))
        .order_by_asc(Column::CreatedAt)
        .all(&ctx.db)
        .await?;

    let mut counts = BTreeMap::<String, usize>::new();
    for item in items {
        let Some(paragraph_key) = item.paragraph_key.as_deref() else {
            continue;
        };
        let key = paragraph_key.trim();
        if key.is_empty() {
            continue;
        }

        *counts.entry(key.to_string()).or_default() += 1;
    }

    let summary = counts
        .into_iter()
        .map(|(paragraph_key, count)| ParagraphCommentSummary {
            paragraph_key,
            count,
        })
        .collect::<Vec<_>>();

    format::json(summary)
}

#[debug_handler]
pub async fn captcha_challenge(
    headers: HeaderMap,
    connect_info: Result<ConnectInfo<SocketAddr>, ExtensionRejection>,
) -> Result<Response> {
    let ip_address = detect_client_ip(&headers, connect_info.as_ref().ok());
    format::json(comment_guard::create_captcha_challenge(
        ip_address.as_deref(),
    )?)
}

#[debug_handler]
pub async fn add(
    State(ctx): State<AppContext>,
    headers: HeaderMap,
    connect_info: Result<ConnectInfo<SocketAddr>, ExtensionRejection>,
    Json(params): Json<CreateCommentRequest>,
) -> Result<Response> {
    let scope = normalized_scope(params.scope.clone())?;
    let post_slug = if let Some(post_slug) = params.post_slug.as_deref() {
        Some(post_slug.to_string())
    } else if let Some(post_id) = params.post_id.as_deref() {
        resolve_post_slug(&ctx, post_id).await?
    } else {
        None
    }
    .and_then(|value| normalize_optional_string(Some(value)));

    let author = normalize_optional_string(params.author);
    let email = normalize_optional_string(params.email);
    let avatar = normalize_optional_string(params.avatar);
    let content = normalize_optional_string(params.content);
    let ip_address = detect_client_ip(&headers, connect_info.as_ref().ok());
    let user_agent = normalize_with_limit(header_value(&headers, header::USER_AGENT), 512);
    let referer = normalize_with_limit(header_value(&headers, header::REFERER), 1024);
    let paragraph_key = normalize_optional_string(params.paragraph_key);
    let paragraph_excerpt = normalize_optional_string(params.paragraph_excerpt)
        .or_else(|| content.as_deref().and_then(preview_excerpt));

    if post_slug.is_none() {
        return Err(Error::BadRequest("post_slug is required".to_string()));
    }

    if author.is_none() {
        return Err(Error::BadRequest("author is required".to_string()));
    }

    if content.is_none() {
        return Err(Error::BadRequest("content is required".to_string()));
    }

    if scope == PARAGRAPH_SCOPE && paragraph_key.is_none() {
        return Err(Error::BadRequest("paragraph_key is required".to_string()));
    }

    comment_guard::enforce_comment_guard(
        &ctx,
        &CommentGuardInput {
            ip_address: ip_address.as_deref(),
            email: email.as_deref(),
            user_agent: user_agent.as_deref(),
            author: author.as_deref(),
            content: content.as_deref(),
            honeypot_website: params.website.as_deref(),
            turnstile_token: params.turnstile_token.as_deref(),
            captcha_token: params.captcha_token.as_deref(),
            captcha_answer: params.captcha_answer.as_deref(),
        },
    )
    .await?;

    let mut item = ActiveModel {
        ..Default::default()
    };
    item.post_id = Set(params
        .post_id
        .as_deref()
        .and_then(|value| Uuid::parse_str(value).ok()));
    item.post_slug = Set(post_slug);
    item.author = Set(author);
    item.email = Set(email);
    item.avatar = Set(avatar);
    item.ip_address = Set(ip_address);
    item.user_agent = Set(user_agent);
    item.referer = Set(referer);
    item.content = Set(content);
    item.scope = Set(scope);
    item.paragraph_key = Set(paragraph_key);
    item.paragraph_excerpt = Set(paragraph_excerpt);
    item.reply_to = Set(params
        .reply_to
        .as_deref()
        .and_then(|value| Uuid::parse_str(value).ok()));
    item.reply_to_comment_id = Set(params.reply_to_comment_id);
    item.approved = Set(Some(params.approved.unwrap_or(false)));
    let item = item.insert(&ctx.db).await?;
    notifications::notify_new_comment(&ctx, &item).await;
    format::json(item)
}

#[debug_handler]
pub async fn update(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
    Json(params): Json<Params>,
) -> Result<Response> {
    let actor = check_auth(&headers)?;
    let item = load_item(&ctx, id).await?;
    let mut item = item.into_active_model();
    params.update(&mut item);
    let item = item.update(&ctx.db).await?;
    admin_audit::log_event(
        &ctx,
        Some(&actor),
        "comment.update",
        "comment",
        Some(item.id.to_string()),
        item.post_slug.clone(),
        Some(serde_json::json!({ "approved": item.approved })),
    )
    .await?;
    format::json(item)
}

#[debug_handler]
pub async fn remove(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    let actor = check_auth(&headers)?;
    let item = load_item(&ctx, id).await?;
    let label = item.post_slug.clone();
    item.delete(&ctx.db).await?;
    admin_audit::log_event(
        &ctx,
        Some(&actor),
        "comment.delete",
        "comment",
        Some(id.to_string()),
        label,
        None,
    )
    .await?;
    format::empty()
}

#[debug_handler]
pub async fn get_one(
    headers: HeaderMap,
    Path(id): Path<i32>,
    State(ctx): State<AppContext>,
) -> Result<Response> {
    check_auth(&headers)?;
    format::json(load_item(&ctx, id).await?)
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/comments/")
        .add("captcha", get(captcha_challenge))
        .add("/", get(list))
        .add("paragraphs/summary", get(paragraph_summary))
        .add("/", post(add))
        .add("{id}", get(get_one))
        .add("{id}", delete(remove))
        .add("{id}", put(update))
        .add("{id}", patch(update))
}
