use crate::core::chunk::types::{Chunk, ChunkRule, ChunkType};
use crate::core::db::schema;
use crate::core::db::PostgresDb;
use anyhow::{Context, Result};
use serde_json::Value;
use sqlx::{PgPool, Row};
use tracing::info;

pub async fn execute_rule1(db: &PostgresDb, file_uuid: &str, fps: f64) -> Result<usize> {
    let pool = db.pool();
    let pre_chunks_table = schema::table_name("pre_chunks");

    let asr_segments = fetch_asr_segments(pool, file_uuid, &pre_chunks_table).await?;

    let asrx_segments = fetch_asrx_segments(pool, file_uuid, &pre_chunks_table).await?;

    let yolo_frames = fetch_yolo_frames(pool, file_uuid, &pre_chunks_table).await?;

    let face_frames = fetch_face_frames(pool, file_uuid).await?;

    let video = db
        .get_video_by_uuid(file_uuid)
        .await?
        .context("Video not found")?;

    let file_id = video.id;

    let mut count = 0;
    let mut tx = pool.begin().await?;

    for (idx, seg) in asr_segments.iter().enumerate() {
        let speaker_id = find_best_speaker(&seg, &asrx_segments);

        let yolo_objects = find_yolo_objects(seg.start_frame, seg.end_frame, &yolo_frames);

        let face_ids = find_face_ids(seg.start_frame, seg.end_frame, &face_frames);

        let metadata = serde_json::json!({
            "speaker_id": speaker_id,
            "yolo_objects": yolo_objects,
            "face_ids": face_ids,
            "language": seg.language,
        });

        let content = serde_json::json!({
            "text": seg.text,
            "text_normalized": seg.text.to_lowercase(),
        });

        let chunk = Chunk::from_seconds(
            file_id as i32,
            file_uuid.to_string(),
            idx as u32,
            ChunkType::Sentence,
            ChunkRule::Rule1,
            seg.start_time,
            seg.end_time,
            fps,
            content,
        )
        .with_metadata(metadata);

        db.store_chunk_in_tx(&chunk, &mut tx).await?;

        count += 1;

        if count % 100 == 0 {
            info!(
                "Rule 1: Processed {} segments for video {}",
                count, file_uuid
            );
        }
    }

    tx.commit().await?;

    info!(
        "Rule 1 completed: {} sentence chunks created for video {}",
        count, file_uuid
    );

    Ok(count)
}

#[derive(Debug, Clone)]
struct AsrSegment {
    start_frame: i64,
    end_frame: i64,
    start_time: f64,
    end_time: f64,
    text: String,
    language: String,
}

#[derive(Debug, Clone)]
struct AsrxSegment {
    start_frame: i64,
    end_frame: i64,
    start_time: f64,
    end_time: f64,
    speaker: String,
}

#[derive(Debug, Clone)]
struct YoloFrame {
    frame: i64,
    detections: Vec<YoloDetection>,
}

#[derive(Debug, Clone)]
struct YoloDetection {
    class_name: String,
    confidence: f64,
}

#[derive(Debug, Clone)]
struct FaceFrame {
    frame: i64,
    faces: Vec<FaceDetection>,
}

#[derive(Debug, Clone)]
struct FaceDetection {
    face_id: String,
    confidence: f64,
    identity_id: Option<i32>,
}

async fn fetch_asr_segments(
    pool: &PgPool,
    file_uuid: &str,
    table: &str,
) -> Result<Vec<AsrSegment>> {
    let query = format!(
        r#"
        SELECT 
            start_frame, end_frame, start_time, end_time, data
        FROM {}
        WHERE file_uuid = $1 AND processor_type = 'asr'
        ORDER BY start_frame
        "#,
        table
    );

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

    let segments: Vec<AsrSegment> = rows
        .iter()
        .map(|row| {
            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 start_time: f64 = row.try_get("start_time").unwrap_or(0.0);
            let end_time: f64 = row.try_get("end_time").unwrap_or(0.0);
            let data: Value = row.try_get("data").unwrap_or(Value::Null);

            let text = data.get("text").and_then(|t| t.as_str()).unwrap_or("");
            let language = data
                .get("language")
                .and_then(|l| l.as_str())
                .unwrap_or("unknown");

            AsrSegment {
                start_frame,
                end_frame,
                start_time,
                end_time,
                text: text.to_string(),
                language: language.to_string(),
            }
        })
        .collect();

    Ok(segments)
}

async fn fetch_asrx_segments(
    pool: &PgPool,
    file_uuid: &str,
    table: &str,
) -> Result<Vec<AsrxSegment>> {
    let query = format!(
        r#"
        SELECT 
            start_frame, end_frame, start_time, end_time, data
        FROM {}
        WHERE file_uuid = $1 AND processor_type = 'asrx'
        ORDER BY start_frame
        "#,
        table
    );

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

    let segments: Vec<AsrxSegment> = rows
        .iter()
        .map(|row| {
            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 start_time: f64 = row.try_get("start_time").unwrap_or(0.0);
            let end_time: f64 = row.try_get("end_time").unwrap_or(0.0);
            let data: Value = row.try_get("data").unwrap_or(Value::Null);

            let speaker = data
                .get("speaker")
                .and_then(|s| s.as_str())
                .unwrap_or("UNKNOWN");

            AsrxSegment {
                start_frame,
                end_frame,
                start_time,
                end_time,
                speaker: speaker.to_string(),
            }
        })
        .collect();

    Ok(segments)
}

async fn fetch_yolo_frames(pool: &PgPool, file_uuid: &str, table: &str) -> Result<Vec<YoloFrame>> {
    let query = format!(
        r#"
        SELECT 
            coordinate_index as frame, data
        FROM {}
        WHERE file_uuid = $1 AND processor_type = 'yolo'
        ORDER BY coordinate_index
        "#,
        table
    );

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

    let frames: Vec<YoloFrame> = rows
        .iter()
        .map(|row| {
            let frame: i64 = row.try_get("frame").unwrap_or(0);
            let data: Value = row.try_get("data").unwrap_or(Value::Null);

            let detections: Vec<YoloDetection> = data
                .get("detections")
                .and_then(|d| d.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|det| {
                            let class_name = det.get("class_name").and_then(|c| c.as_str());
                            let confidence = det.get("confidence").and_then(|c| c.as_f64());

                            if class_name.is_some() && confidence.is_some() {
                                Some(YoloDetection {
                                    class_name: class_name.unwrap().to_string(),
                                    confidence: confidence.unwrap(),
                                })
                            } else {
                                None
                            }
                        })
                        .collect()
                })
                .unwrap_or_default();

            YoloFrame { frame, detections }
        })
        .collect();

    Ok(frames)
}

async fn fetch_face_frames(pool: &PgPool, file_uuid: &str) -> Result<Vec<FaceFrame>> {
    let face_detections_table = schema::table_name("face_detections");

    let query = format!(
        r#"
        SELECT 
            frame_number as frame,
            face_id,
            confidence,
            identity_id
        FROM {}
        WHERE file_uuid = $1
        ORDER BY frame_number
        "#,
        face_detections_table
    );

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

    let mut frame_map: std::collections::HashMap<i64, FaceFrame> = std::collections::HashMap::new();

    for row in rows {
        let frame: i64 = row.try_get("frame").unwrap_or(0);
        let face_id: Option<String> = row.try_get("face_id").ok();
        let confidence: f64 = row.try_get("confidence").unwrap_or(0.0);
        let identity_id: Option<i32> = row.try_get("identity_id").ok();

        if let Some(face_id) = face_id {
            let detection = FaceDetection {
                face_id: face_id.clone(),
                confidence,
                identity_id,
            };

            frame_map
                .entry(frame)
                .or_insert_with(|| FaceFrame {
                    frame,
                    faces: Vec::new(),
                })
                .faces
                .push(detection);
        }
    }

    let mut frames: Vec<FaceFrame> = frame_map.into_values().collect();
    frames.sort_by_key(|f| f.frame);

    Ok(frames)
}

fn find_best_speaker(asr_seg: &AsrSegment, asrx_segments: &[AsrxSegment]) -> String {
    let mut best_speaker = "UNKNOWN".to_string();
    let mut max_overlap = 0.0f64;

    for spk in asrx_segments {
        let overlap =
            (asr_seg.end_time.min(spk.end_time) - asr_seg.start_time.max(spk.start_time)).max(0.0);
        if overlap > max_overlap {
            max_overlap = overlap;
            best_speaker = spk.speaker.clone();
        }
    }

    best_speaker
}

fn find_yolo_objects(start_frame: i64, end_frame: i64, yolo_frames: &[YoloFrame]) -> Vec<String> {
    let mut objects = Vec::new();

    for frame in yolo_frames {
        if frame.frame >= start_frame && frame.frame <= end_frame {
            for det in &frame.detections {
                if det.confidence > 0.5 && !objects.contains(&det.class_name) {
                    objects.push(det.class_name.clone());
                }
            }
        }
    }

    objects
}

fn find_face_ids(start_frame: i64, end_frame: i64, face_frames: &[FaceFrame]) -> Vec<String> {
    let mut face_ids = Vec::new();

    for frame in face_frames {
        if frame.frame >= start_frame && frame.frame <= end_frame {
            for face in &frame.faces {
                if face.confidence > 0.5 && !face_ids.contains(&face.face_id) {
                    face_ids.push(face.face_id.clone());
                }
            }
        }
    }

    face_ids
}
