use chrono::{DateTime, Datelike, Duration, Utc};
use db::models::ai::{AiPlan, AiPlanRepository, UserAiSubscription, UserAiSubscriptionRepository};
use sqlx::PgPool;
use uuid::Uuid;

#[derive(Debug, thiserror::Error)]
pub enum PlanError {
    #[error("Database error: {0}")]
    Db(#[from] sqlx::Error),
    #[error("No active subscription")]
    NoSubscription,
    #[error("Subscription expired")]
    SubscriptionExpired,
    #[error("Feature '{feature}' not allowed by plan")]
    FeatureNotAllowed { feature: String },
    #[error("Model '{model}' not allowed by plan")]
    ModelNotAllowed { model: String },
    #[error("Customer role has no AI access")]
    CustomerRoleForbidden,
}

impl PlanError {
    pub fn status_code(&self) -> axum::http::StatusCode {
        use axum::http::StatusCode;
        match self {
            PlanError::NoSubscription => StatusCode::PAYMENT_REQUIRED,
            PlanError::SubscriptionExpired => StatusCode::PAYMENT_REQUIRED,
            PlanError::FeatureNotAllowed { .. } => StatusCode::FORBIDDEN,
            PlanError::ModelNotAllowed { .. } => StatusCode::FORBIDDEN,
            PlanError::CustomerRoleForbidden => StatusCode::FORBIDDEN,
            PlanError::Db(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    pub fn error_body(&self) -> serde_json::Value {
        serde_json::json!({
            "error": self.to_string(),
            "code": match self {
                PlanError::NoSubscription => "NO_AI_SUBSCRIPTION",
                PlanError::SubscriptionExpired => "AI_SUBSCRIPTION_EXPIRED",
                PlanError::FeatureNotAllowed { .. } => "AI_FEATURE_NOT_ALLOWED",
                PlanError::ModelNotAllowed { .. } => "AI_MODEL_NOT_ALLOWED",
                PlanError::CustomerRoleForbidden => "AI_CUSTOMER_FORBIDDEN",
                PlanError::Db(_) => "INTERNAL_ERROR",
            }
        })
    }
}

const CUSTOMER_ROLE: &str = "customer";

/// Resolves an active subscription, ensuring the user is not a customer and the
/// current billing period is valid. This should be called before any AI action.
pub async fn require_active_subscription(
    pool: &PgPool,
    user_id: Uuid,
    role_code: Option<&str>,
) -> Result<(UserAiSubscription, AiPlan), PlanError> {
    if role_code.map(|r| r.eq_ignore_ascii_case(CUSTOMER_ROLE)).unwrap_or(false) {
        return Err(PlanError::CustomerRoleForbidden);
    }

    let sub = UserAiSubscriptionRepository::get_by_user_id(pool, user_id)
        .await?
        .ok_or(PlanError::NoSubscription)?;

    let now = Utc::now();
    if now < sub.current_period_start || now >= sub.current_period_end {
        return Err(PlanError::SubscriptionExpired);
    }

    let plan = AiPlanRepository::get_by_id(pool, sub.plan_id)
        .await?
        .ok_or_else(|| {
            tracing::error!("Subscription references unknown plan_id={}", sub.plan_id);
            PlanError::NoSubscription
        })?;

    Ok((sub, plan))
}

pub fn is_feature_allowed(plan: &AiPlan, feature_code: &str) -> bool {
    plan.allowed_features
        .as_array()
        .map(|arr| arr.iter().any(|v| v.as_str() == Some(feature_code)))
        .unwrap_or(false)
}

pub fn is_model_allowed(plan: &AiPlan, model_alias: &str) -> bool {
    plan.allowed_models
        .as_array()
        .map(|arr| arr.iter().any(|v| v.as_str() == Some(model_alias)))
        .unwrap_or(false)
}

pub fn require_feature(plan: &AiPlan, feature_code: &str) -> Result<(), PlanError> {
    if is_feature_allowed(plan, feature_code) {
        Ok(())
    } else {
        Err(PlanError::FeatureNotAllowed {
            feature: feature_code.to_string(),
        })
    }
}

#[allow(dead_code)]
pub fn require_model(plan: &AiPlan, model_alias: &str) -> Result<(), PlanError> {
    if is_model_allowed(plan, model_alias) {
        Ok(())
    } else {
        Err(PlanError::ModelNotAllowed {
            model: model_alias.to_string(),
        })
    }
}

/// Idempotently create a Free subscription for a user if none exists.
pub async fn ensure_free_subscription(
    pool: &PgPool,
    user_id: Uuid,
    role_code: Option<&str>,
) -> Result<(UserAiSubscription, AiPlan), PlanError> {
    if let Some(existing) = UserAiSubscriptionRepository::get_by_user_id(pool, user_id).await? {
            let plan = AiPlanRepository::get_by_id(pool, existing.plan_id)
            .await?
            .unwrap_or_else(|| {
                tracing::warn!("Could not resolve plan for subscription {}", existing.id);
                // Return a synthetic plan to avoid panic; real deployments should fix data.
                AiPlan {
                    id: existing.plan_id,
                    code: "unknown".into(),
                    name: "Unknown".into(),
                    monthly_credits: 0,
                    daily_action_limit: 0,
                    allowed_models: serde_json::Value::Array(vec![]),
                    allowed_features: serde_json::Value::Array(vec![]),
                    is_active: false,
                    created_at: existing.created_at,
                    updated_at: existing.updated_at,
                }
            });
        return Ok((existing, plan));
    }

    let plan = AiPlanRepository::get_by_code(pool, "free")
        .await?
        .ok_or(PlanError::NoSubscription)?;

    let now = Utc::now();
    let start = now.with_day(1).unwrap_or(now).with_time(chrono::NaiveTime::MIN).unwrap();
    let end = (start + Duration::days(32))
        .with_day(1)
        .unwrap_or(start + Duration::days(32))
        .with_time(chrono::NaiveTime::MIN)
        .unwrap();

    let sub = UserAiSubscriptionRepository::create(
        pool,
        user_id,
        plan.id,
        role_code,
        plan.monthly_credits,
        start,
        end,
    )
    .await?;

    Ok((sub, plan))
}

/// Recompute the start/end of the current monthly period.
pub fn current_monthly_period(now: DateTime<Utc>) -> (DateTime<Utc>, DateTime<Utc>) {
    let start = now
        .with_day(1)
        .unwrap_or(now)
        .with_time(chrono::NaiveTime::MIN)
        .unwrap();
    let end = (start + Duration::days(32))
        .with_day(1)
        .unwrap_or(start + Duration::days(32))
        .with_time(chrono::NaiveTime::MIN)
        .unwrap();
    (start, end)
}
