use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};

use chrono::{DateTime, Duration, Utc};
use loco_rs::prelude::*;
use sea_orm::{ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
use serde::Serialize;
use uuid::Uuid;

use crate::models::_entities::{comment_blacklist, comments};

const DEFAULT_RATE_LIMIT_WINDOW_SECONDS: i64 = 10 * 60;
const DEFAULT_RATE_LIMIT_MAX_PER_WINDOW: u64 = 8;
const DEFAULT_MIN_INTERVAL_SECONDS: i64 = 12;
const DEFAULT_CAPTCHA_TTL_SECONDS: i64 = 10 * 60;

const ENV_RATE_LIMIT_WINDOW_SECONDS: &str = "TERMI_COMMENT_RATE_LIMIT_WINDOW_SECONDS";
const ENV_RATE_LIMIT_MAX_PER_WINDOW: &str = "TERMI_COMMENT_RATE_LIMIT_MAX_PER_WINDOW";
const ENV_MIN_INTERVAL_SECONDS: &str = "TERMI_COMMENT_MIN_INTERVAL_SECONDS";
const ENV_BLOCK_KEYWORDS: &str = "TERMI_COMMENT_BLOCK_KEYWORDS";
const ENV_CAPTCHA_TTL_SECONDS: &str = "TERMI_COMMENT_CAPTCHA_TTL_SECONDS";

pub const MATCHER_TYPE_IP: &str = "ip";
pub const MATCHER_TYPE_EMAIL: &str = "email";
pub const MATCHER_TYPE_USER_AGENT: &str = "user_agent";

#[derive(Clone, Debug, Serialize)]
pub struct CommentCaptchaChallenge {
    pub token: String,
    pub question: String,
    pub expires_in_seconds: i64,
}

#[derive(Clone, Debug)]
pub struct CommentGuardInput<'a> {
    pub ip_address: Option<&'a str>,
    pub email: Option<&'a str>,
    pub user_agent: Option<&'a str>,
    pub author: Option<&'a str>,
    pub content: Option<&'a str>,
    pub honeypot_website: Option<&'a str>,
    pub turnstile_token: Option<&'a str>,
    pub captcha_token: Option<&'a str>,
    pub captcha_answer: Option<&'a str>,
}

#[derive(Clone, Debug)]
struct GuardConfig {
    rate_limit_window_seconds: i64,
    rate_limit_max_per_window: u64,
    min_interval_seconds: i64,
    blocked_keywords: Vec<String>,
    captcha_ttl_seconds: i64,
}

#[derive(Clone, Debug)]
struct CaptchaEntry {
    answer: String,
    expires_at: DateTime<Utc>,
    ip_address: Option<String>,
}

fn parse_env_i64(name: &str, fallback: i64, min: i64, max: i64) -> i64 {
    std::env::var(name)
        .ok()
        .and_then(|value| value.trim().parse::<i64>().ok())
        .map(|value| value.clamp(min, max))
        .unwrap_or(fallback)
}

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

fn normalize_email(value: Option<&str>) -> Option<String> {
    trim_to_option(value).map(|item| item.to_lowercase())
}

fn normalize_user_agent(value: Option<&str>) -> Option<String> {
    trim_to_option(value).map(|item| item.chars().take(512).collect::<String>())
}

fn normalize_ip(value: Option<&str>) -> Option<String> {
    trim_to_option(value).map(|item| item.chars().take(96).collect::<String>())
}

fn parse_keywords() -> Vec<String> {
    std::env::var(ENV_BLOCK_KEYWORDS)
        .ok()
        .map(|value| {
            value
                .split([',', '\n', '\r'])
                .map(str::trim)
                .filter(|item| !item.is_empty())
                .map(|item| item.to_lowercase())
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

fn load_config() -> GuardConfig {
    GuardConfig {
        rate_limit_window_seconds: parse_env_i64(
            ENV_RATE_LIMIT_WINDOW_SECONDS,
            DEFAULT_RATE_LIMIT_WINDOW_SECONDS,
            10,
            24 * 60 * 60,
        ),
        rate_limit_max_per_window: parse_env_i64(
            ENV_RATE_LIMIT_MAX_PER_WINDOW,
            DEFAULT_RATE_LIMIT_MAX_PER_WINDOW as i64,
            1,
            500,
        ) as u64,
        min_interval_seconds: parse_env_i64(
            ENV_MIN_INTERVAL_SECONDS,
            DEFAULT_MIN_INTERVAL_SECONDS,
            0,
            6 * 60 * 60,
        ),
        blocked_keywords: parse_keywords(),
        captcha_ttl_seconds: parse_env_i64(
            ENV_CAPTCHA_TTL_SECONDS,
            DEFAULT_CAPTCHA_TTL_SECONDS,
            30,
            24 * 60 * 60,
        ),
    }
}

fn captcha_store() -> &'static Mutex<HashMap<String, CaptchaEntry>> {
    static STORE: OnceLock<Mutex<HashMap<String, CaptchaEntry>>> = OnceLock::new();
    STORE.get_or_init(|| Mutex::new(HashMap::new()))
}

fn cleanup_expired_captcha_entries(store: &mut HashMap<String, CaptchaEntry>, now: DateTime<Utc>) {
    let expired = store
        .iter()
        .filter_map(|(token, entry)| (entry.expires_at <= now).then_some(token.clone()))
        .collect::<Vec<_>>();

    for token in expired {
        store.remove(&token);
    }
}

pub fn normalize_matcher_type(value: &str) -> Option<&'static str> {
    match value.trim().to_ascii_lowercase().as_str() {
        MATCHER_TYPE_IP => Some(MATCHER_TYPE_IP),
        MATCHER_TYPE_EMAIL => Some(MATCHER_TYPE_EMAIL),
        MATCHER_TYPE_USER_AGENT | "ua" | "useragent" => Some(MATCHER_TYPE_USER_AGENT),
        _ => None,
    }
}

pub fn normalize_matcher_value(matcher_type: &str, raw_value: &str) -> Option<String> {
    let normalized_type = normalize_matcher_type(matcher_type)?;

    match normalized_type {
        MATCHER_TYPE_IP => normalize_ip(Some(raw_value)),
        MATCHER_TYPE_EMAIL => normalize_email(Some(raw_value)),
        MATCHER_TYPE_USER_AGENT => normalize_user_agent(Some(raw_value)),
        _ => None,
    }
}

pub fn create_captcha_challenge(client_ip: Option<&str>) -> Result<CommentCaptchaChallenge> {
    let config = load_config();
    let seed = Uuid::new_v4().as_u128();
    let left = ((seed % 9) + 1) as i64;
    let right = (((seed / 11) % 9) + 1) as i64;
    let use_subtract = seed % 2 == 0 && left > right;
    let (question, answer) = if use_subtract {
        (
            format!("{} - {} = ?", left, right),
            (left - right).to_string(),
        )
    } else {
        (
            format!("{} + {} = ?", left, right),
            (left + right).to_string(),
        )
    };

    let token = Uuid::new_v4().to_string();
    let now = Utc::now();
    let expires_at = now + Duration::seconds(config.captcha_ttl_seconds);
    let ip_address = normalize_ip(client_ip);

    let mut store = captcha_store()
        .lock()
        .map_err(|_| Error::InternalServerError)?;
    cleanup_expired_captcha_entries(&mut store, now);
    store.insert(
        token.clone(),
        CaptchaEntry {
            answer,
            expires_at,
            ip_address,
        },
    );

    Ok(CommentCaptchaChallenge {
        token,
        question,
        expires_in_seconds: config.captcha_ttl_seconds,
    })
}

pub fn verify_captcha_solution(
    captcha_token: Option<&str>,
    captcha_answer: Option<&str>,
    client_ip: Option<&str>,
) -> Result<()> {
    let token = trim_to_option(captcha_token)
        .ok_or_else(|| Error::BadRequest("请先完成验证码".to_string()))?;
    let answer = trim_to_option(captcha_answer)
        .ok_or_else(|| Error::BadRequest("请填写验证码答案".to_string()))?;

    let now = Utc::now();
    let normalized_ip = normalize_ip(client_ip);

    let mut store = captcha_store()
        .lock()
        .map_err(|_| Error::InternalServerError)?;
    cleanup_expired_captcha_entries(&mut store, now);

    let Some(entry) = store.remove(&token) else {
        return Err(Error::BadRequest("验证码已失效，请刷新后重试".to_string()));
    };

    if entry.expires_at <= now {
        return Err(Error::BadRequest("验证码已过期，请刷新后重试".to_string()));
    }

    if entry
        .ip_address
        .as_deref()
        .zip(normalized_ip.as_deref())
        .is_some_and(|(expected, current)| expected != current)
    {
        return Err(Error::BadRequest(
            "验证码校验失败，请刷新后重试".to_string(),
        ));
    }

    if entry.answer.trim() != answer.trim() {
        return Err(Error::BadRequest("验证码答案错误".to_string()));
    }

    Ok(())
}

async fn check_blacklist(ctx: &AppContext, input: &CommentGuardInput<'_>) -> Result<()> {
    let now = Utc::now();
    let candidates = [
        (MATCHER_TYPE_IP, normalize_ip(input.ip_address)),
        (MATCHER_TYPE_EMAIL, normalize_email(input.email)),
        (
            MATCHER_TYPE_USER_AGENT,
            normalize_user_agent(input.user_agent),
        ),
    ];

    for (matcher_type, matcher_value) in candidates {
        let Some(matcher_value) = matcher_value else {
            continue;
        };

        let matched = comment_blacklist::Entity::find()
            .filter(comment_blacklist::Column::MatcherType.eq(matcher_type))
            .filter(comment_blacklist::Column::MatcherValue.eq(&matcher_value))
            .filter(
                Condition::any()
                    .add(comment_blacklist::Column::Active.is_null())
                    .add(comment_blacklist::Column::Active.eq(true)),
            )
            .filter(
                Condition::any()
                    .add(comment_blacklist::Column::ExpiresAt.is_null())
                    .add(comment_blacklist::Column::ExpiresAt.gt(now)),
            )
            .one(&ctx.db)
            .await?;

        if matched.is_some() {
            return Err(Error::BadRequest("评论请求已被拦截".to_string()));
        }
    }

    Ok(())
}

async fn check_rate_limit(ctx: &AppContext, input: &CommentGuardInput<'_>) -> Result<()> {
    let config = load_config();
    let Some(ip_address) = normalize_ip(input.ip_address) else {
        return Ok(());
    };

    let now = Utc::now();
    let since = now - Duration::seconds(config.rate_limit_window_seconds);

    let count = comments::Entity::find()
        .filter(comments::Column::IpAddress.eq(&ip_address))
        .filter(comments::Column::CreatedAt.gte(since))
        .count(&ctx.db)
        .await?;

    if count >= config.rate_limit_max_per_window {
        return Err(Error::BadRequest("评论过于频繁，请稍后再试".to_string()));
    }

    if config.min_interval_seconds <= 0 {
        return Ok(());
    }

    if let Some(last_comment) = comments::Entity::find()
        .filter(comments::Column::IpAddress.eq(&ip_address))
        .order_by_desc(comments::Column::CreatedAt)
        .one(&ctx.db)
        .await?
    {
        let last_created_at = DateTime::<Utc>::from(last_comment.created_at);
        let elapsed = now.signed_duration_since(last_created_at).num_seconds();
        if elapsed < config.min_interval_seconds {
            return Err(Error::BadRequest("提交过快，请稍后再试".to_string()));
        }
    }

    Ok(())
}

fn contains_blocked_keyword(input: &CommentGuardInput<'_>) -> Option<String> {
    let config = load_config();
    if config.blocked_keywords.is_empty() {
        return None;
    }

    let mut merged = String::new();
    for value in [input.author, input.email, input.content] {
        if let Some(value) = value {
            merged.push_str(value);
            merged.push('\n');
        }
    }
    let lower = merged.to_lowercase();

    config
        .blocked_keywords
        .into_iter()
        .find(|keyword| lower.contains(keyword))
}

pub async fn enforce_comment_guard(ctx: &AppContext, input: &CommentGuardInput<'_>) -> Result<()> {
    if trim_to_option(input.honeypot_website).is_some() {
        return Err(Error::BadRequest("提交未通过校验".to_string()));
    }

    let settings = crate::controllers::site_settings::load_current(ctx).await?;
    match crate::services::turnstile::effective_mode(
        &settings,
        crate::services::turnstile::TurnstileScope::Comment,
    ) {
        crate::services::turnstile::VerificationMode::Off => {}
        crate::services::turnstile::VerificationMode::Captcha => {
            verify_captcha_solution(input.captcha_token, input.captcha_answer, input.ip_address)?;
        }
        crate::services::turnstile::VerificationMode::Turnstile => {
            crate::services::turnstile::verify_token(
                &settings,
                input.turnstile_token,
                input.ip_address,
            )
            .await?;
        }
    }

    if contains_blocked_keyword(input).is_some() {
        return Err(Error::BadRequest("评论内容包含敏感关键词".to_string()));
    }

    check_blacklist(ctx, input).await?;
    check_rate_limit(ctx, input).await?;

    Ok(())
}
