use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use chrono::Utc;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use super::error::{ClosureError, Result};
use super::fsutil::atomic_write;

const SYNC_FORMAT: &str = "closure-sync";
const SYNC_VERSION: u32 = 1;
const MANIFEST_FILE: &str = "sync-manifest.json";
const IGNORED_ENTRIES: [&str; 2] = [".git", "sync-manifest.json"];

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncManifest {
    pub format: String,
    pub version: u32,
    pub snapshot_id: String,
    pub created_at: String,
    pub files: BTreeMap<String, SyncFileMeta>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SyncFileMeta {
    pub size: u64,
    pub sha256: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncOutcome {
    NoChange,
    UpdatedFromSource,
    UpdatedFromDestination,
}

/// Bidirectionally synchronizes files between `src` and `dst` using manifest/hash comparison.
pub fn sync_dirs(src: &Path, dst: &Path) -> Result<SyncOutcome> {
    fs::create_dir_all(src)?;
    fs::create_dir_all(dst)?;

    let src_snapshot = snapshot_dir(src)?;
    let dst_snapshot = snapshot_dir(dst)?;

    let src_manifest_opt = read_manifest(src)?;
    let dst_manifest_opt = read_manifest(dst)?;

    if let Some(ref manifest) = src_manifest_opt {
        validate_manifest(src, manifest)?;
    }
    if let Some(ref manifest) = dst_manifest_opt {
        validate_manifest(dst, manifest)?;
    }

    if src_snapshot == dst_snapshot {
        let manifest = build_manifest(&src_snapshot);
        write_manifest(src, &manifest)?;
        write_manifest(dst, &manifest)?;
        return Ok(SyncOutcome::NoChange);
    }

    if src_manifest_opt.is_none() && dst_manifest_opt.is_none() {
        if src_snapshot.is_empty() && !dst_snapshot.is_empty() {
            let manifest = build_manifest(&dst_snapshot);
            apply_snapshot(src, &dst_snapshot)?;
            write_manifest(src, &manifest)?;
            write_manifest(dst, &manifest)?;
            return Ok(SyncOutcome::UpdatedFromDestination);
        }
        if dst_snapshot.is_empty() && !src_snapshot.is_empty() {
            let manifest = build_manifest(&src_snapshot);
            apply_snapshot(dst, &src_snapshot)?;
            write_manifest(src, &manifest)?;
            write_manifest(dst, &manifest)?;
            return Ok(SyncOutcome::UpdatedFromSource);
        }
        return Err(ClosureError::SyncConflict(
            "both sides changed before a sync baseline was established".to_string(),
        ));
    }

    if src_manifest_opt.is_none() {
        let manifest = dst_manifest_opt.as_ref().expect("checked above");
        if src_snapshot.is_empty() {
            apply_snapshot(src, &dst_snapshot)?;
            write_manifest(src, manifest)?;
            write_manifest(dst, manifest)?;
            return Ok(SyncOutcome::UpdatedFromDestination);
        }
        return Err(ClosureError::SyncConflict(
            "source missing sync baseline and contains local changes".to_string(),
        ));
    }

    if dst_manifest_opt.is_none() {
        let manifest = src_manifest_opt.as_ref().expect("checked above");
        if dst_snapshot.is_empty() {
            apply_snapshot(dst, &src_snapshot)?;
            write_manifest(src, manifest)?;
            write_manifest(dst, manifest)?;
            return Ok(SyncOutcome::UpdatedFromSource);
        }
        return Err(ClosureError::SyncConflict(
            "destination missing sync baseline and contains local changes".to_string(),
        ));
    }

    let src_manifest = src_manifest_opt.expect("checked above");
    let dst_manifest = dst_manifest_opt.expect("checked above");

    if src_snapshot == dst_snapshot {
        write_manifest(src, &src_manifest)?;
        write_manifest(dst, &dst_manifest)?;
        return Ok(SyncOutcome::NoChange);
    }

    let src_changed = has_changed_since_manifest(&src_snapshot, &src_manifest);
    let dst_changed = has_changed_since_manifest(&dst_snapshot, &dst_manifest);

    match (src_changed, dst_changed) {
        (true, true) => Err(ClosureError::SyncConflict(
            "both workdata and sync contain divergent changes".to_string(),
        )),
        (true, false) => {
            apply_snapshot(dst, &src_snapshot)?;
            let manifest = build_manifest(&src_snapshot);
            write_manifest(src, &manifest)?;
            write_manifest(dst, &manifest)?;
            Ok(SyncOutcome::UpdatedFromSource)
        }
        (false, true) => {
            apply_snapshot(src, &dst_snapshot)?;
            let manifest = build_manifest(&dst_snapshot);
            write_manifest(src, &manifest)?;
            write_manifest(dst, &manifest)?;
            Ok(SyncOutcome::UpdatedFromDestination)
        }
        (false, false) => Err(ClosureError::SyncConflict(
            "snapshots differ but neither side has a local change marker; manual intervention required"
                .to_string(),
        )),
    }
}

pub fn sha256_hex(bytes: &[u8]) -> String {
    let digest = Sha256::digest(bytes);
    format!("{digest:x}")
}

fn snapshot_dir(dir: &Path) -> Result<BTreeMap<String, Vec<u8>>> {
    let mut snapshot = BTreeMap::new();
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name().to_string_lossy().to_string();
        if IGNORED_ENTRIES.contains(&name.as_str()) {
            continue;
        }
        if path.is_file() {
            snapshot.insert(name, fs::read(path)?);
        }
    }
    Ok(snapshot)
}

fn build_manifest(snapshot: &BTreeMap<String, Vec<u8>>) -> SyncManifest {
    let mut files = BTreeMap::new();
    for (name, bytes) in snapshot {
        files.insert(
            name.clone(),
            SyncFileMeta {
                size: bytes.len() as u64,
                sha256: sha256_hex(bytes),
            },
        );
    }
    SyncManifest {
        format: SYNC_FORMAT.to_string(),
        version: SYNC_VERSION,
        snapshot_id: Utc::now()
            .timestamp_nanos_opt()
            .unwrap_or_default()
            .to_string(),
        created_at: Utc::now().to_rfc3339(),
        files,
    }
}

fn apply_snapshot(dir: &Path, snapshot: &BTreeMap<String, Vec<u8>>) -> Result<()> {
    let existing: BTreeSet<String> = fs::read_dir(dir)?
        .filter_map(|entry| entry.ok())
        .map(|entry| entry.file_name().to_string_lossy().to_string())
        .filter(|name| !IGNORED_ENTRIES.contains(&name.as_str()))
        .collect();

    for name in existing {
        if !snapshot.contains_key(&name) {
            fs::remove_file(dir.join(name))?;
        }
    }

    for (name, bytes) in snapshot {
        atomic_write(&dir.join(name), bytes)?;
    }
    Ok(())
}

fn read_manifest(dir: &Path) -> Result<Option<SyncManifest>> {
    let path = manifest_path(dir);
    if !path.exists() {
        return Ok(None);
    }
    let contents = fs::read_to_string(path)?;
    let manifest = serde_json::from_str(&contents)?;
    Ok(Some(manifest))
}

fn write_manifest(dir: &Path, manifest: &SyncManifest) -> Result<()> {
    let bytes = serde_json::to_vec_pretty(manifest)?;
    atomic_write(&manifest_path(dir), &bytes)
}

fn manifest_path(dir: &Path) -> PathBuf {
    dir.join(MANIFEST_FILE)
}

fn validate_manifest(dir: &Path, manifest: &SyncManifest) -> Result<()> {
    if manifest.format != SYNC_FORMAT {
        return Err(ClosureError::Validation(format!(
            "unsupported sync format in {}",
            manifest_path(dir).display()
        )));
    }
    if manifest.version == 0 {
        return Err(ClosureError::Validation(format!(
            "invalid sync manifest version in {}",
            manifest_path(dir).display()
        )));
    }

    for (name, meta) in &manifest.files {
        let path = dir.join(name);
        if !path.exists() {
            continue;
        }
        let bytes = fs::read(&path)?;
        if bytes.len() as u64 != meta.size || sha256_hex(&bytes) != meta.sha256 {
            return Err(ClosureError::SyncConflict(format!(
                "manifest checksum mismatch for {}",
                path.display()
            )));
        }
    }
    Ok(())
}

fn has_changed_since_manifest(
    snapshot: &BTreeMap<String, Vec<u8>>,
    manifest: &SyncManifest,
) -> bool {
    let current = build_manifest(snapshot);
    current.files != manifest.files
}
