use anyhow::Result;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::time::{Duration, Instant};
use walkdir::WalkDir;

use crate::consts::{EXT_CONTENT, EXT_SNIPPET, EXT_TEMPLATE};
use crate::engine::cache::get_subtree; // subtree helper is public
use crate::engine::Engine;
use crate::fsutil::{ensure_parent, path_no_ext};
use toml::Value as TomlValue;

pub fn run_watch(mut engine: Engine) -> Result<()> {
    engine.full_build()?;

    let (tx, rx) = std::sync::mpsc::channel::<notify::Result<Event>>();
    let mut watcher: RecommendedWatcher = Watcher::new(tx, Config::default())?;

    watcher.watch(&engine.src_dir, RecursiveMode::Recursive)?;
    watcher.watch(&engine.content_dir, RecursiveMode::Recursive)?;
    watcher.watch(&engine.snippets_dir, RecursiveMode::Recursive)?;
    if engine.static_dir.exists() {
        watcher.watch(&engine.static_dir, RecursiveMode::Recursive)?;
    }

    log::info!("watching… (Ctrl-C to quit)");

    let mut last_run = Instant::now();

    loop {
        match rx.recv() {
            Ok(Ok(event)) => {
                if last_run.elapsed() < Duration::from_millis(30) {
                    continue;
                }
                last_run = Instant::now();

                if handle_event(&mut engine, &event).is_err() {
                    log::warn!("[watch] error handling event: {:?}", event);
                }
            }
            Ok(Err(e)) => {
                log::warn!("[watch] watcher error: {}", e);
            }
            Err(_) => break,
        }
    }

    Ok(())
}

fn handle_event(engine: &mut Engine, ev: &Event) -> Result<()> {
    match ev.kind {
        EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_) | EventKind::Any => {
            for p in ev.paths.iter() {
                if p.is_dir() {
                    continue;
                }

                let ext = p
                    .extension()
                    .and_then(|s| s.to_str())
                    .unwrap_or("")
                    .to_ascii_lowercase();

                if is_under(p, &engine.static_dir) {
                    copy_single_static(engine, p)?;
                    continue;
                }

                if is_under(p, &engine.snippets_dir) {
                    if ext == EXT_SNIPPET {
                        let pc = p.canonicalize().unwrap_or(p.to_path_buf());
                        if let Some(rel_no_ext) = path_no_ext(&pc, &engine.snippets_dir) {
                            let rel = rel_no_ext + ".htmls";
                            engine.invalidate_snippet(&rel);
                            rebuild_templates_for_snippet(engine, &rel)?;
                        }
                    }
                    continue;
                }

                if is_under(p, &engine.content_dir) {
                    if ext == EXT_CONTENT {
                        let pc = p.canonicalize().unwrap_or(p.to_path_buf());
                        if let Some(rel_no_ext) = path_no_ext(&pc, &engine.content_dir) {
                            let rel = rel_no_ext + ".toml";
                            // pointer-aware rebuild
                            pointer_aware_rebuild_for_content(engine, &rel, &pc)?;
                        }
                    }
                    continue;
                }

                if is_under(p, &engine.src_dir) && ext == EXT_TEMPLATE {
                    let pc = p.canonicalize().unwrap_or(p.to_path_buf());
                    engine.build_template(pc.as_path())?;
                }
            }
        }
        _ => {}
    }
    Ok(())
}

fn pointer_aware_rebuild_for_content(
    engine: &mut Engine,
    cont_rel: &str,
    abs_file: &Path,
) -> Result<()> {
    engine.depgraph.clear_content_file(cont_rel);
    let old_toml: Option<TomlValue> = engine.caches.peek_toml(cont_rel);

    // bust caches for this file
    engine.invalidate_content(cont_rel);

    // fresh TOML (if exists)
    let new_toml: Option<TomlValue> = std::fs::read_to_string(abs_file)
        .ok()
        .and_then(|s| toml::from_str::<TomlValue>(&s).ok());

    // 1) templates that depend on the whole file (no pointer)
    let mut to_rebuild: Vec<String> = engine.depgraph.templates_by_content(cont_rel);

    // 2) pointer-level diffs
    let pointer_keys = engine.depgraph.pointer_keys_for_content_file(cont_rel);
    if let (Some(oldv), Some(newv)) = (old_toml.as_ref(), new_toml.as_ref()) {
        for key in pointer_keys {
            // "<file>.toml::path=<dot.path>"
            let path = key.split("::path=").nth(1).unwrap_or("").to_string();
            let changed = if path.is_empty() {
                oldv != newv
            } else {
                let old_sub = get_subtree(oldv, &path).ok();
                let new_sub = get_subtree(newv, &path).ok();
                old_sub != new_sub
            };
            if changed {
                let mut tpls = engine.depgraph.templates_by_content_ptr(&key);
                to_rebuild.append(&mut tpls);
            }
        }
    } else {
        // unknown diff → rebuild all pointer users for this file
        for key in pointer_keys {
            let mut tpls = engine.depgraph.templates_by_content_ptr(&key);
            to_rebuild.append(&mut tpls);
        }
    }

    // dedup and rebuild
    to_rebuild.sort();
    to_rebuild.dedup();
    if to_rebuild.is_empty() {
        // depgraph cold start → rebuild all templates to repopulate
        for entry in WalkDir::new(&engine.src_dir) {
            let entry = entry?;
            let p = entry.path();
            if entry.file_type().is_file()
                && p.extension().and_then(|s| s.to_str()).unwrap_or("") == EXT_TEMPLATE
            {
                engine.build_template(p)?;
            }
        }
        return Ok(());
    }

    for tpl_rel in to_rebuild {
        engine.build_template(&engine.src_dir.join(&tpl_rel))?;
    }
    Ok(())
}

fn rebuild_templates_for_snippet(engine: &mut Engine, snip_rel: &str) -> Result<()> {
    let affected = engine.depgraph.templates_by_snippet(snip_rel);
    if affected.is_empty() {
        for entry in WalkDir::new(&engine.src_dir) {
            let entry = entry?;
            let p = entry.path();
            if entry.file_type().is_file()
                && p.extension().and_then(|s| s.to_str()).unwrap_or("") == EXT_TEMPLATE
            {
                engine.build_template(p)?;
            }
        }
        return Ok(());
    }

    for tpl_rel in affected {
        engine.build_template(&engine.src_dir.join(&tpl_rel))?;
    }
    Ok(())
}

fn is_under(path: &Path, root: &Path) -> bool {
    let Ok(p) = path.canonicalize() else { return false; };
    let Ok(r) = root.canonicalize() else { return false; };
    p.starts_with(&r)
}

fn copy_single_static(engine: &Engine, abs_file: &Path) -> Result<()> {
    let rel = abs_file.strip_prefix(&engine.static_dir).unwrap_or(abs_file);
    let out = engine.out_dir.join(rel);
    ensure_parent(&out)?;
    if needs_copy(abs_file, &out) {
        std::fs::copy(abs_file, &out)?;
        if engine.verbose {
            eprintln!("[static] {}", out.display());
        }
    }
    Ok(())
}

fn needs_copy(from: &Path, to: &Path) -> bool {
    use std::fs::Metadata;
    let meta_src: Metadata = match std::fs::metadata(from) {
        Ok(m) => m,
        Err(_) => return false,
    };
    let meta_dst: Metadata = match std::fs::metadata(to) {
        Ok(m) => m,
        Err(_) => return true,
    };

    let size_diff = meta_src.len() != meta_dst.len();
    let time_diff = meta_src.modified().ok() != meta_dst.modified().ok();
    size_diff || time_diff
}
