use axum::{
    extract::State,
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use sqlx::Row;

use crate::api::server::AppState;
use crate::core::db::PostgresDb;

pub fn five_w1h_agent_routes() -> Router<AppState> {
    Router::new()
        .route("/api/v1/agents/5w1h/analyze", post(analyze_5w1h))
        .route("/api/v1/agents/5w1h/batch", post(batch_analyze_5w1h))
        .route("/api/v1/agents/5w1h/status", get(get_5w1h_status))
}

#[derive(Debug, Deserialize)]
pub struct Analyze5W1HRequest {
    pub file_uuid: String,
    pub scene_group_size: Option<usize>,
    pub model: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct Analyze5W1HResponse {
    pub success: bool,
    pub file_uuid: String,
    pub summaries: Vec<SummaryChunk>,
    pub processing_status: FiveW1HProcessingStatus,
}

#[derive(Debug, Serialize)]
pub struct SummaryChunk {
    pub chunk_id: String,
    pub summary: String,
    pub analysis_5w1h: FiveW1HAnalysis,
    pub start_frame: i64,
    pub end_frame: i64,
    pub start_time: f64,
    pub end_time: f64,
    pub fps: f64,
    pub scene_count: usize,
}

#[derive(Debug, Serialize)]
pub struct FiveW1HAnalysis {
    pub who: Vec<String>,
    pub what: Vec<String>,
    pub location: Vec<String>,
    pub when: String,
    pub why: String,
    pub how: String,
}

#[derive(Debug, Serialize)]
pub struct FiveW1HProcessingStatus {
    pub status: String,
    pub scenes_processed: i32,
    pub scenes_total: i32,
    pub progress_pct: f64,
}

#[derive(Debug, Deserialize)]
pub struct BatchAnalyze5W1HRequest {
    pub file_uuids: Vec<String>,
    pub scene_group_size: Option<usize>,
}

#[derive(Debug, Serialize)]
pub struct BatchAnalyze5W1HResponse {
    pub success: bool,
    pub jobs: Vec<BatchJobStatus>,
}

#[derive(Debug, Serialize)]
pub struct BatchJobStatus {
    pub file_uuid: String,
    pub status: String,
    pub message: String,
}

async fn analyze_5w1h(
    State(state): State<AppState>,
    Json(req): Json<Analyze5W1HRequest>,
) -> Result<Json<Analyze5W1HResponse>, (StatusCode, String)> {
    let db = PostgresDb::from_pool(state.db.pool().clone());

    let scene_group_size = req.scene_group_size.unwrap_or(7);
    let model = req.model.unwrap_or_else(|| "gemma4:latest".to_string());

    let rule3_chunks = fetch_rule3_chunks(&db, &req.file_uuid)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    if rule3_chunks.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            "No Rule 3 chunks found for this video".to_string(),
        ));
    }

    let scenes_total = rule3_chunks.len() as i32;

    update_agent_status(&db, &req.file_uuid, "running", 0, scenes_total, 0.0)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    let summaries =
        process_scene_groups(&db, &req.file_uuid, &rule3_chunks, scene_group_size, &model)
            .await
            .map_err(|e| {
                tracing::error!("Failed to process scene groups: {}", e);
                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
            })?;

    let scenes_processed = rule3_chunks.len() as i32;
    let progress_pct = 100.0;

    update_agent_status(
        &db,
        &req.file_uuid,
        "completed",
        scenes_processed,
        scenes_total,
        progress_pct,
    )
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok(Json(Analyze5W1HResponse {
        success: true,
        file_uuid: req.file_uuid,
        summaries,
        processing_status: FiveW1HProcessingStatus {
            status: "completed".to_string(),
            scenes_processed,
            scenes_total,
            progress_pct,
        },
    }))
}

async fn batch_analyze_5w1h(
    State(state): State<AppState>,
    Json(req): Json<BatchAnalyze5W1HRequest>,
) -> Result<Json<BatchAnalyze5W1HResponse>, (StatusCode, String)> {
    let scene_group_size = req.scene_group_size.unwrap_or(7);
    let jobs: Vec<BatchJobStatus> = req
        .file_uuids
        .iter()
        .map(|uuid| {
            let uuid_clone = uuid.clone();
            let db_clone = PostgresDb::from_pool(state.db.pool().clone());
            let group_size = scene_group_size;

            tokio::spawn(async move {
                let _ = process_single_video_5w1h(&db_clone, &uuid_clone, group_size).await;
            });

            BatchJobStatus {
                file_uuid: uuid.clone(),
                status: "queued".to_string(),
                message: "Job queued for async processing".to_string(),
            }
        })
        .collect();

    Ok(Json(BatchAnalyze5W1HResponse {
        success: true,
        jobs,
    }))
}

async fn get_5w1h_status(
    State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
    let db = PostgresDb::from_pool(state.db.pool().clone());

    let videos_with_5w1h = fetch_videos_with_5w1h_status(&db)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

    Ok(Json(serde_json::json!({
        "success": true,
        "videos": videos_with_5w1h
    })))
}

async fn fetch_rule3_chunks(db: &PostgresDb, file_uuid: &str) -> anyhow::Result<Vec<Rule3Chunk>> {
    let table = crate::core::db::schema::table_name("chunks");
    let query = format!(
        r#"
        SELECT chunk_id, start_frame, end_frame, fps, content, metadata
        FROM {}
        WHERE uuid = $1 AND (chunk_type = 'scene' OR chunk_type = 'cut')
        ORDER BY start_frame
        "#,
        table
    );

    let rows = sqlx::query(&query)
        .bind(file_uuid)
        .fetch_all(db.pool())
        .await?;

    let chunks: Vec<Rule3Chunk> = rows
        .iter()
        .map(|row| {
            let chunk_id: String = row.try_get("chunk_id").unwrap_or_default();
            let start_frame: i64 = row.try_get("start_frame").unwrap_or(0);
            let end_frame: i64 = row.try_get("end_frame").unwrap_or(0);
            let fps: f64 = row.try_get("fps").unwrap_or(30.0);
            let content: serde_json::Value =
                row.try_get("content").unwrap_or(serde_json::Value::Null);
            let metadata: serde_json::Value =
                row.try_get("metadata").unwrap_or(serde_json::Value::Null);

            let summary = content
                .get("data")
                .and_then(|d| d.get("scene_number"))
                .and_then(|s| s.as_u64())
                .map(|n| format!("Scene {}", n))
                .unwrap_or_else(|| {
                    content
                        .get("data")
                        .and_then(|d| d.get("summary"))
                        .and_then(|s| s.as_str())
                        .unwrap_or("No summary")
                        .to_string()
                });

            Rule3Chunk {
                chunk_id,
                start_frame,
                end_frame,
                fps,
                summary,
                metadata,
            }
        })
        .collect();

    Ok(chunks)
}

async fn process_scene_groups(
    db: &PostgresDb,
    file_uuid: &str,
    rule3_chunks: &[Rule3Chunk],
    group_size: usize,
    model: &str,
) -> anyhow::Result<Vec<SummaryChunk>> {
    let mut summaries = Vec::new();
    let chunks_total = rule3_chunks.len();

    for (group_idx, group) in rule3_chunks.chunks(group_size).enumerate() {
        let context_text = group
            .iter()
            .map(|c| c.summary.clone())
            .collect::<Vec<_>>()
            .join("\n\n");

        let faces = aggregate_faces(group);
        let objects = aggregate_objects(group);

        let llm_result = call_llm_for_5w1h(&context_text, &faces, &objects, model).await?;

        let start_frame = group.first().map(|c| c.start_frame).unwrap_or(0);
        let end_frame = group.last().map(|c| c.end_frame).unwrap_or(0);
        let fps = group.first().map(|c| c.fps).unwrap_or(30.0);

        let start_time = start_frame as f64 / fps;
        let end_time = end_frame as f64 / fps;

        let chunk_id = format!("summary_{}_{}", file_uuid, group_idx);

        let summary = SummaryChunk {
            chunk_id: chunk_id.clone(),
            summary: llm_result.summary,
            analysis_5w1h: llm_result.analysis_5w1h,
            start_frame,
            end_frame,
            start_time,
            end_time,
            fps,
            scene_count: group.len(),
        };

        store_summary_chunk(db, file_uuid, &summary, group).await?;

        let scenes_processed = ((group_idx + 1) * group_size).min(chunks_total) as i32;
        let progress_pct = (scenes_processed as f64 / chunks_total as f64) * 100.0;

        update_agent_status(
            db,
            file_uuid,
            "running",
            scenes_processed,
            chunks_total as i32,
            progress_pct,
        )
        .await?;

        summaries.push(summary);
    }

    Ok(summaries)
}

async fn call_llm_for_5w1h(
    context_text: &str,
    faces: &[String],
    objects: &[String],
    model: &str,
) -> anyhow::Result<LLM5W1HResult> {
    let system_prompt = r#"You are a video scene analysis assistant for Momentry Core.

## Task:
Analyze the provided video scenes and extract structured 5W1H information.

## Output Format (JSON):
{
  "summary": "A brief 2-3 sentence summary of these scenes",
  "5w1h": {
    "who": ["List of main characters/actors"],
    "what": ["List of main events/actions"],
    "where": ["List of locations/settings"],
    "when": "Time of day or temporal context",
    "why": "Motivation or reason for events",
    "how": "Method or process used"
  }
}

## Guidelines:
- Keep summaries concise and natural
- Extract key information, not details
- Return ONLY valid JSON, no explanations"#;

    let prompt = format!(
        r#"Analyze these video scenes:

## Scene Summaries:
{}

## Detected Faces:
{}

## Detected Objects:
{}

Return the 5W1H analysis in JSON format."#,
        context_text,
        faces.join(", "),
        objects.join(", ")
    );

    let client = Client::new();
    let ollama_url = "http://localhost:11434/api/generate";

    let body = serde_json::json!({
        "model": model,
        "prompt": prompt,
        "system": system_prompt,
        "stream": false,
        "format": "json"
    });

    let response = client
        .post(ollama_url)
        .json(&body)
        .timeout(std::time::Duration::from_secs(60))
        .send()
        .await?;

    let ollama_resp: serde_json::Value = response.json().await?;

    let response_text = ollama_resp
        .get("response")
        .and_then(|v| v.as_str())
        .unwrap_or("{}");

    let parsed: serde_json::Value = serde_json::from_str(response_text)?;

    let summary = parsed
        .get("summary")
        .and_then(|s| s.as_str())
        .unwrap_or("No summary generated")
        .to_string();

    let analysis_5w1h = FiveW1HAnalysis {
        who: parsed
            .get("5w1h")
            .and_then(|w| w.get("who"))
            .and_then(|w| w.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default(),
        what: parsed
            .get("5w1h")
            .and_then(|w| w.get("what"))
            .and_then(|w| w.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default(),
        location: parsed
            .get("5w1h")
            .and_then(|w| w.get("where"))
            .and_then(|w| w.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
                    .collect()
            })
            .unwrap_or_default(),
        when: parsed
            .get("5w1h")
            .and_then(|w| w.get("when"))
            .and_then(|w| w.as_str())
            .unwrap_or("unknown")
            .to_string(),
        why: parsed
            .get("5w1h")
            .and_then(|w| w.get("why"))
            .and_then(|w| w.as_str())
            .unwrap_or("unknown")
            .to_string(),
        how: parsed
            .get("5w1h")
            .and_then(|w| w.get("how"))
            .and_then(|w| w.as_str())
            .unwrap_or("unknown")
            .to_string(),
    };

    Ok(LLM5W1HResult {
        summary,
        analysis_5w1h,
    })
}

async fn store_summary_chunk(
    db: &PostgresDb,
    file_uuid: &str,
    summary: &SummaryChunk,
    group: &[Rule3Chunk],
) -> anyhow::Result<()> {
    let table = crate::core::db::schema::table_name("chunks");

    let content = serde_json::json!({
        "rule": "rule4",
        "data": {
            "summary": summary.summary,
            "5w1h": {
                "who": summary.analysis_5w1h.who,
                "what": summary.analysis_5w1h.what,
                "where": summary.analysis_5w1h.location,
                "when": summary.analysis_5w1h.when,
                "why": summary.analysis_5w1h.why,
                "how": summary.analysis_5w1h.how,
            }
        }
    });

    let metadata = serde_json::json!({
        "scene_count": summary.scene_count,
        "scene_chunk_ids": group.iter().map(|c| c.chunk_id.clone()).collect::<Vec<_>>(),
    });

    let query = format!(
        r#"
        INSERT INTO {} (
            uuid, chunk_id, chunk_index, chunk_type, 
            start_frame, end_frame, fps, start_time, end_time, content, metadata
        )
        VALUES ($1, $2, $3, 'summary', $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb)
        ON CONFLICT (uuid, chunk_id) DO UPDATE SET
            content = EXCLUDED.content,
            metadata = EXCLUDED.metadata,
            updated_at = CURRENT_TIMESTAMP
        "#,
        table
    );

    let start_time = summary.start_time;
    let end_time = summary.end_time;

    sqlx::query(&query)
        .bind(file_uuid)
        .bind(&summary.chunk_id)
        .bind(0)
        .bind(summary.start_frame)
        .bind(summary.end_frame)
        .bind(summary.fps)
        .bind(start_time)
        .bind(end_time)
        .bind(&content)
        .bind(&metadata)
        .execute(db.pool())
        .await?;

    Ok(())
}

async fn update_agent_status(
    db: &PostgresDb,
    file_uuid: &str,
    status: &str,
    scenes_processed: i32,
    scenes_total: i32,
    progress_pct: f64,
) -> anyhow::Result<()> {
    let table = crate::core::db::schema::table_name("videos");

    let agent_status = serde_json::json!({
        "five_w1h": {
            "status": status,
            "scenes_processed": scenes_processed,
            "scenes_total": scenes_total,
            "progress_pct": progress_pct
        }
    });

    let query = format!(
        r#"
        UPDATE {}
        SET processing_status = jsonb_set(
            COALESCE(processing_status, '{{}}'::jsonb),
            '{{agents}}',
            $1::jsonb
        )
        WHERE uuid = $2
        "#,
        table
    );

    sqlx::query(&query)
        .bind(&agent_status)
        .bind(file_uuid)
        .execute(db.pool())
        .await?;

    Ok(())
}

async fn fetch_videos_with_5w1h_status(db: &PostgresDb) -> anyhow::Result<Vec<serde_json::Value>> {
    let table = crate::core::db::schema::table_name("videos");

    let query = format!(
        r#"
        SELECT uuid, processing_status->'agents'->'five_w1h' as agent_status
        FROM {}
        WHERE processing_status->'agents'->'five_w1h' IS NOT NULL
        ORDER BY updated_at DESC
        LIMIT 50
        "#,
        table
    );

    let rows = sqlx::query(&query).fetch_all(db.pool()).await?;

    let videos: Vec<serde_json::Value> = rows
        .iter()
        .map(|row| {
            let uuid: String = row.try_get("uuid").unwrap_or_default();
            let status: Option<serde_json::Value> = row.try_get("agent_status").ok();

            serde_json::json!({
                "uuid": uuid,
                "five_w1h_status": status
            })
        })
        .collect();

    Ok(videos)
}

async fn process_single_video_5w1h(
    db: &PostgresDb,
    file_uuid: &str,
    scene_group_size: usize,
) -> anyhow::Result<()> {
    let rule3_chunks = fetch_rule3_chunks(db, file_uuid).await?;

    if rule3_chunks.is_empty() {
        return Ok(());
    }

    let scenes_total = rule3_chunks.len() as i32;

    update_agent_status(db, file_uuid, "running", 0, scenes_total, 0.0).await?;

    let _ = process_scene_groups(
        db,
        file_uuid,
        &rule3_chunks,
        scene_group_size,
        "gemma4:latest",
    )
    .await?;

    update_agent_status(
        db,
        file_uuid,
        "completed",
        scenes_total,
        scenes_total,
        100.0,
    )
    .await?;

    Ok(())
}

fn aggregate_faces(group: &[Rule3Chunk]) -> Vec<String> {
    group
        .iter()
        .flat_map(|c| {
            c.metadata
                .get("faces")
                .and_then(|f| f.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default()
        })
        .collect()
}

fn aggregate_objects(group: &[Rule3Chunk]) -> Vec<String> {
    group
        .iter()
        .flat_map(|c| {
            c.metadata
                .get("objects")
                .and_then(|o| o.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default()
        })
        .collect()
}

#[derive(Debug, Clone)]
struct Rule3Chunk {
    chunk_id: String,
    start_frame: i64,
    end_frame: i64,
    fps: f64,
    summary: String,
    metadata: serde_json::Value,
}

#[derive(Debug)]
struct LLM5W1HResult {
    summary: String,
    analysis_5w1h: FiveW1HAnalysis,
}
