use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::path::Path;
use std::sync::Arc;

use crate::db::DbConnection as Connection;
use chrono::{Datelike, Local, TimeZone};
use serde::Serialize;

use crate::db::queries::generated_file_hash as qhash;
use crate::model::{GeneratedFileHash, Post};
use crate::util::{atomic_write_str, content_hash, file_hash, now_unix_ms};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeneratedWriteOutcome {
    Written,
    SkippedUnchanged,
}

pub(crate) struct GeneratedFileWriter<'a> {
    conn: &'a Connection,
    output_dir: &'a Path,
    project_id: &'a str,
    existing: Arc<HashMap<String, String>>,
    pending: HashMap<String, GeneratedFileHash>,
    force: bool,
    verify_output: bool,
}

impl<'a> GeneratedFileWriter<'a> {
    pub(crate) fn new(
        conn: &'a Connection,
        output_dir: &'a Path,
        project_id: &'a str,
        force: bool,
        verify_output: bool,
    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        let existing = Arc::new(
            qhash::list_generated_file_hashes(conn, project_id)?
                .into_iter()
                .map(|hash| (hash.relative_path, hash.content_hash))
                .collect(),
        );
        Self::with_existing(conn, output_dir, project_id, existing, force, verify_output)
    }

    pub(crate) fn with_existing(
        conn: &'a Connection,
        output_dir: &'a Path,
        project_id: &'a str,
        existing: Arc<HashMap<String, String>>,
        force: bool,
        verify_output: bool,
    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
        Ok(Self {
            conn,
            output_dir,
            project_id,
            existing,
            pending: HashMap::new(),
            force,
            verify_output,
        })
    }

    pub(crate) fn write_str(
        &mut self,
        relative_path: &str,
        content: &str,
    ) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
        self.write(relative_path, content.as_bytes(), |path| {
            atomic_write_str(path, content)
        })
    }

    pub(crate) fn write_bytes(
        &mut self,
        relative_path: &str,
        content: &[u8],
    ) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
        self.write(relative_path, content, |path| {
            crate::util::atomic_write(path, content)
        })
    }

    fn write(
        &mut self,
        relative_path: &str,
        content: &[u8],
        write: impl FnOnce(&Path) -> std::io::Result<()>,
    ) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
        let hash = content_hash(content);
        let target_path = self.output_dir.join(relative_path);
        let existing_hash = self
            .pending
            .get(relative_path)
            .map(|pending| &pending.content_hash)
            .or_else(|| self.existing.get(relative_path));
        if !self.force
            && existing_hash == Some(&hash)
            && target_path.exists()
            && (!self.verify_output || file_hash(&target_path)? == hash)
        {
            return Ok(GeneratedWriteOutcome::SkippedUnchanged);
        }
        if let Some(parent) = target_path.parent() {
            fs::create_dir_all(parent)?;
        }
        write(&target_path)?;
        self.pending.insert(
            relative_path.to_string(),
            GeneratedFileHash {
                project_id: self.project_id.to_string(),
                relative_path: relative_path.to_string(),
                content_hash: hash,
                updated_at: now_unix_ms(),
            },
        );
        Ok(GeneratedWriteOutcome::Written)
    }

    pub(crate) fn finish(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        qhash::upsert_generated_file_hashes(
            self.conn,
            &self.pending.into_values().collect::<Vec<_>>(),
        )?;
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CalendarArchiveData {
    pub years: BTreeMap<String, usize>,
    pub months: BTreeMap<String, usize>,
    pub days: BTreeMap<String, usize>,
}

pub fn write_generated_file(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &str,
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    write_generated_file_with_mode(
        conn,
        output_dir,
        project_id,
        relative_path,
        content,
        false,
        false,
    )
}

pub fn write_generated_file_verified(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &str,
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    write_generated_file_with_mode(
        conn,
        output_dir,
        project_id,
        relative_path,
        content,
        false,
        true,
    )
}

pub fn write_generated_file_forced(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &str,
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    write_generated_file_with_mode(
        conn,
        output_dir,
        project_id,
        relative_path,
        content,
        true,
        false,
    )
}

fn write_generated_file_with_mode(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &str,
    force: bool,
    verify_output: bool,
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    let hash = content_hash(content.as_bytes());
    let target_path = output_dir.join(relative_path);
    if !force
        && let Ok(existing) = qhash::get_generated_file_hash(conn, project_id, relative_path)
        && existing.content_hash == hash
        && target_path.exists()
        && (!verify_output || file_hash(&target_path)? == hash)
    {
        return Ok(GeneratedWriteOutcome::SkippedUnchanged);
    }

    if let Some(parent) = target_path.parent() {
        fs::create_dir_all(parent)?;
    }
    atomic_write_str(&target_path, content)?;

    qhash::upsert_generated_file_hash(
        conn,
        &GeneratedFileHash {
            project_id: project_id.to_string(),
            relative_path: relative_path.to_string(),
            content_hash: hash,
            updated_at: now_unix_ms(),
        },
    )?;

    Ok(GeneratedWriteOutcome::Written)
}

pub fn write_generated_bytes(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &[u8],
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    write_generated_bytes_with_force(conn, output_dir, project_id, relative_path, content, false)
}

pub fn write_generated_bytes_forced(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &[u8],
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    write_generated_bytes_with_force(conn, output_dir, project_id, relative_path, content, true)
}

fn write_generated_bytes_with_force(
    conn: &Connection,
    output_dir: &Path,
    project_id: &str,
    relative_path: &str,
    content: &[u8],
    force: bool,
) -> Result<GeneratedWriteOutcome, Box<dyn std::error::Error + Send + Sync>> {
    let hash = content_hash(content);
    let target_path = output_dir.join(relative_path);
    if !force
        && let Ok(existing) = qhash::get_generated_file_hash(conn, project_id, relative_path)
        && existing.content_hash == hash
        && target_path.exists()
    {
        return Ok(GeneratedWriteOutcome::SkippedUnchanged);
    }

    if let Some(parent) = target_path.parent() {
        fs::create_dir_all(parent)?;
    }
    crate::util::atomic_write(&target_path, content)?;

    qhash::upsert_generated_file_hash(
        conn,
        &GeneratedFileHash {
            project_id: project_id.to_string(),
            relative_path: relative_path.to_string(),
            content_hash: hash,
            updated_at: now_unix_ms(),
        },
    )?;

    Ok(GeneratedWriteOutcome::Written)
}

pub fn build_core_generation_paths(main_language: &str, blog_languages: &[String]) -> Vec<String> {
    let mut paths = vec![
        "index.html".to_string(),
        "404.html".to_string(),
        "sitemap.xml".to_string(),
        "rss.xml".to_string(),
        "atom.xml".to_string(),
        "calendar.json".to_string(),
    ];

    for language in blog_languages {
        if language != main_language {
            paths.push(format!("{language}/index.html"));
            paths.push(format!("{language}/404.html"));
            paths.push(format!("{language}/rss.xml"));
            paths.push(format!("{language}/atom.xml"));
        }
    }

    paths
}

pub fn build_calendar_archive_data(posts: &[Post]) -> CalendarArchiveData {
    let mut years = BTreeMap::new();
    let mut months = BTreeMap::new();
    let mut days = BTreeMap::new();

    for post in posts {
        let Some(created_at) = Local.timestamp_millis_opt(post.created_at).single() else {
            continue;
        };

        let year = created_at.year().to_string();
        let month = format!("{year}-{:02}", created_at.month());
        let day = format!("{month}-{:02}", created_at.day());

        *years.entry(year).or_insert(0) += 1;
        *months.entry(month).or_insert(0) += 1;
        *days.entry(day).or_insert(0) += 1;
    }

    CalendarArchiveData {
        years,
        months,
        days,
    }
}

pub fn build_calendar_json(posts: &[Post]) -> serde_json::Result<String> {
    serde_json::to_string(&build_calendar_archive_data(posts))
}
