use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::core::chunk::types::{Chunk, ChunkRule, ChunkType};

pub struct MongoDb {
    base_url: String,
    database: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkDocument {
    pub uuid: String,
    pub chunk_id: String,
    pub chunk_index: u32,
    pub chunk_type: String,
    pub start_time: f64,
    pub end_time: f64,
    pub fps: f64,
    pub start_frame: i64,
    pub end_frame: i64,
    pub content: serde_json::Value,
    pub metadata: Option<serde_json::Value>,
    pub vector_id: Option<String>,
    pub parent_chunk_id: Option<String>,
    pub child_chunk_ids: Vec<String>,
}

impl From<Chunk> for ChunkDocument {
    fn from(chunk: Chunk) -> Self {
        let start_time = chunk.start_time().seconds();
        let end_time = chunk.end_time().seconds();
        Self {
            uuid: chunk.uuid,
            chunk_id: chunk.chunk_id,
            chunk_index: chunk.chunk_index,
            chunk_type: chunk.chunk_type.as_str().to_string(),
            start_time,
            end_time,
            fps: chunk.fps,
            start_frame: chunk.start_frame,
            end_frame: chunk.end_frame,
            content: chunk.content,
            metadata: chunk.metadata,
            vector_id: chunk.vector_id,
            parent_chunk_id: chunk.parent_chunk_id,
            child_chunk_ids: chunk.child_chunk_ids,
        }
    }
}

impl MongoDb {
    pub fn new() -> Self {
        let base_url =
            std::env::var("MONGODB_URL").unwrap_or_else(|_| "http://localhost:27017".to_string());
        let database = crate::core::config::MONGODB_DATABASE.clone();
        Self { base_url, database }
    }
}

impl Default for MongoDb {
    fn default() -> Self {
        Self::new()
    }
}

impl MongoDb {
    pub async fn store_chunk(&self, chunk: &Chunk) -> Result<()> {
        let doc: ChunkDocument = chunk.clone().into();
        let client = reqwest::Client::new();

        let url = format!("{}/{}/chunks", self.base_url, self.database);

        client
            .post(&url)
            .json(&doc)
            .send()
            .await
            .context("Failed to store chunk in MongoDB")?;

        Ok(())
    }

    pub async fn get_chunks_by_uuid(&self, uuid: &str) -> Result<Vec<Chunk>> {
        let client = reqwest::Client::new();
        let url = format!(
            "{}/{}/chunks?filter={{\"uuid\":\"{}\"}}",
            self.base_url, self.database, uuid
        );

        let response = client
            .get(&url)
            .send()
            .await
            .context("Failed to get chunks from MongoDB")?;

        #[derive(Deserialize)]
        struct MongoResponse {
            documents: Vec<ChunkDocument>,
        }

        let result: MongoResponse = response.json().await?;

        let chunks: Vec<Chunk> = result
            .documents
            .into_iter()
            .map(|doc| {
                let chunk_type = match doc.chunk_type.as_str() {
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "time_based" => ChunkType::TimeBased,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::Sentence,
                };

                Chunk {
                    file_id: 0,
                    uuid: doc.uuid,
                    chunk_id: doc.chunk_id,
                    chunk_index: doc.chunk_index,
                    chunk_type,
                    rule: ChunkRule::Rule1,
                    fps: doc.fps,
                    start_frame: doc.start_frame,
                    end_frame: doc.end_frame,
                    text_content: None,
                    content: doc.content,
                    metadata: doc.metadata,
                    vector_id: doc.vector_id,
                    frame_count: 0,
                    pre_chunk_ids: vec![],
                    parent_chunk_id: doc.parent_chunk_id,
                    child_chunk_ids: doc.child_chunk_ids,
                    visual_stats: None,
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn search_text(&self, query: &str) -> Result<Vec<Chunk>> {
        let client = reqwest::Client::new();
        let url = format!(
            "{}/{}/chunks?filter={{\"$text\":{{\"$search\":\"{}\"}}}}",
            self.base_url, self.database, query
        );

        let response = client
            .get(&url)
            .send()
            .await
            .context("Failed to search text in MongoDB")?;

        #[derive(Deserialize)]
        struct MongoResponse {
            documents: Vec<ChunkDocument>,
        }

        let result: MongoResponse = response.json().await?;

        let chunks: Vec<Chunk> = result
            .documents
            .into_iter()
            .map(|doc| {
                let chunk_type = match doc.chunk_type.as_str() {
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "time" => ChunkType::TimeBased,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::Sentence,
                };

                Chunk {
                    file_id: 0,
                    uuid: doc.uuid,
                    chunk_id: doc.chunk_id,
                    chunk_index: doc.chunk_index,
                    chunk_type,
                    rule: ChunkRule::Rule1,
                    fps: doc.fps,
                    start_frame: doc.start_frame,
                    end_frame: doc.end_frame,
                    text_content: None,
                    content: doc.content,
                    metadata: doc.metadata,
                    vector_id: doc.vector_id,
                    frame_count: 0,
                    pre_chunk_ids: vec![],
                    parent_chunk_id: doc.parent_chunk_id,
                    child_chunk_ids: doc.child_chunk_ids,
                    visual_stats: None,
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn get_all_chunks(&self) -> Result<Vec<Chunk>> {
        let client = reqwest::Client::new();
        let url = format!("{}/{}/chunks", self.base_url, self.database);

        let response = client
            .get(&url)
            .send()
            .await
            .context("Failed to get all chunks from MongoDB")?;

        #[derive(Deserialize)]
        struct MongoResponse {
            documents: Vec<ChunkDocument>,
        }

        let result: MongoResponse = response.json().await?;

        let chunks: Vec<Chunk> = result
            .documents
            .into_iter()
            .map(|doc| {
                let chunk_type = match doc.chunk_type.as_str() {
                    "sentence" => ChunkType::Sentence,
                    "cut" => ChunkType::Cut,
                    "time" => ChunkType::TimeBased,
                    "trace" => ChunkType::Trace,
                    "story" => ChunkType::Story,
                    _ => ChunkType::Sentence,
                };

                Chunk {
                    file_id: 0,
                    uuid: doc.uuid,
                    chunk_id: doc.chunk_id,
                    chunk_index: doc.chunk_index,
                    chunk_type,
                    rule: ChunkRule::Rule1,
                    fps: doc.fps,
                    start_frame: doc.start_frame,
                    end_frame: doc.end_frame,
                    text_content: None,
                    content: doc.content,
                    metadata: doc.metadata,
                    vector_id: doc.vector_id,
                    frame_count: 0,
                    pre_chunk_ids: vec![],
                    parent_chunk_id: doc.parent_chunk_id,
                    child_chunk_ids: doc.child_chunk_ids,
                    visual_stats: None,
                }
            })
            .collect();

        Ok(chunks)
    }

    pub async fn get_chunk_count(&self) -> Result<i64> {
        let chunks = self.get_all_chunks().await?;
        Ok(chunks.len() as i64)
    }
}

#[async_trait]
impl super::Database for MongoDb {
    async fn init() -> Result<Self> {
        Ok(Self::new())
    }
}
