use crate::error::{Error, Result};
use chrono::Utc;
use log::{debug, error, warn};
use notify::Watcher;
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
use tauri::{AppHandle, Listener, Runtime};
use tokio::select;
use tokio::sync::watch;
use tokio::time::sleep;
use yaak_git::{GitWorktreeStatus, git_path_is_ignored, git_repository_paths, git_worktree_status};
use yaak_rpc_schema::GitWatchResult;

const GIT_STATUS_COALESCE_WINDOW: Duration = Duration::from_millis(250);

pub(crate) async fn watch_git_worktree_status<R, F>(
    app_handle: AppHandle<R>,
    dir: &Path,
    on_status: F,
) -> Result<GitWatchResult>
where
    R: Runtime,
    F: Fn(GitWorktreeStatus) + Send + Sync + 'static,
{
    let paths = git_repository_paths(dir)?;
    let repo_dir = dir.to_path_buf();
    let workdir = paths.workdir;
    let gitdir = paths.gitdir;
    let commondir = paths.commondir;

    let (tx, rx) = mpsc::channel::<notify::Result<notify::Event>>();
    let mut watcher = notify::recommended_watcher(tx)
        .map_err(|e| Error::GenericError(format!("Failed to watch Git repository: {e}")))?;

    // Watch only the directory Yaak syncs to, not the whole worktree — the
    // containing repo may be huge and busy (e.g. a sync dir inside a monorepo),
    // and watching it all burns CPU re-checking status for unrelated changes
    watcher
        .watch(&repo_dir, notify::RecursiveMode::Recursive)
        .map_err(|e| Error::GenericError(format!("Failed to watch Git sync directory: {e}")))?;

    // Watch the git metadata that affects branch/status info: the top-level
    // gitdir files (HEAD, index) and refs. Not the whole gitdir, since
    // .git/objects churns constantly during fetches and gc. Refs and
    // packed-refs live in the common dir, which only differs from the gitdir
    // for linked worktrees
    watcher
        .watch(&gitdir, notify::RecursiveMode::NonRecursive)
        .map_err(|e| Error::GenericError(format!("Failed to watch Git metadata: {e}")))?;
    if commondir != gitdir {
        watcher
            .watch(&commondir, notify::RecursiveMode::NonRecursive)
            .map_err(|e| Error::GenericError(format!("Failed to watch Git common dir: {e}")))?;
    }
    let refs_dir = commondir.join("refs");
    if refs_dir.exists() {
        watcher
            .watch(&refs_dir, notify::RecursiveMode::Recursive)
            .map_err(|e| Error::GenericError(format!("Failed to watch Git refs: {e}")))?;
    }

    let (async_tx, mut async_rx) = tokio::sync::mpsc::channel::<notify::Result<notify::Event>>(100);
    std::thread::spawn(move || {
        for res in rx {
            if async_tx.blocking_send(res).is_err() {
                break;
            }
        }
    });

    let (cancel_tx, cancel_rx) = watch::channel(());
    let mut cancel_rx = cancel_rx;
    send_worktree_status(&repo_dir, &on_status);

    tauri::async_runtime::spawn(async move {
        let _watcher = watcher;
        loop {
            select! {
                Some(event_res) = async_rx.recv() => {
                    handle_git_watch_event(
                        event_res,
                        &mut async_rx,
                        &repo_dir,
                        &workdir,
                        &gitdir,
                        &commondir,
                        &on_status,
                    ).await;
                }
                _ = cancel_rx.changed() => {
                    break;
                }
            }
        }
    });

    let app_handle_inner = app_handle.clone();
    let unlisten_event = format!("git-watch-unlisten-{}", Utc::now().timestamp_millis());
    app_handle.listen_any(unlisten_event.clone(), move |event| {
        app_handle_inner.unlisten(event.id());
        if let Err(e) = cancel_tx.send(()) {
            warn!("Failed to send git watch cancel signal {e:?}");
        }
    });

    Ok(GitWatchResult { unlisten_event })
}

async fn handle_git_watch_event(
    event_res: notify::Result<notify::Event>,
    async_rx: &mut tokio::sync::mpsc::Receiver<notify::Result<notify::Event>>,
    repo_dir: &Path,
    workdir: &Path,
    gitdir: &Path,
    commondir: &Path,
    on_status: &impl Fn(GitWorktreeStatus),
) {
    if !is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir, commondir) {
        return;
    }

    send_worktree_status(repo_dir, on_status);

    let settle_window = sleep(GIT_STATUS_COALESCE_WINDOW);
    tokio::pin!(settle_window);
    loop {
        select! {
            Some(event_res) = async_rx.recv() => {
                let _ = is_relevant_git_watch_event(event_res, repo_dir, workdir, gitdir, commondir);
            }
            _ = &mut settle_window => {
                break;
            }
        }
    }

    send_worktree_status(repo_dir, on_status);
}

fn is_relevant_git_watch_event(
    event_res: notify::Result<notify::Event>,
    repo_dir: &Path,
    workdir: &Path,
    gitdir: &Path,
    commondir: &Path,
) -> bool {
    let event = match event_res {
        Ok(event) => event,
        Err(e) => {
            error!("Git watch error: {:?}", e);
            return false;
        }
    };

    for path in event.paths {
        if path.strip_prefix(gitdir).is_ok() || path.strip_prefix(commondir).is_ok() {
            return true;
        }

        let Ok(rela_path) = path.strip_prefix(workdir) else {
            continue;
        };

        match git_path_is_ignored(repo_dir, rela_path) {
            Ok(true) => {}
            Ok(false) => return true,
            Err(e) => {
                debug!("Failed to check Git ignore status for {:?}: {e}", rela_path);
                return true;
            }
        }
    }

    false
}

fn send_worktree_status(repo_dir: &Path, on_status: &impl Fn(GitWorktreeStatus)) {
    match git_worktree_status(repo_dir) {
        Ok(status) => on_status(status),
        Err(e) => {
            warn!("Failed to get git worktree status: {e}");
        }
    }
}
