from __future__ import annotations

"""Background sync loop for selected repositories.

The UI owns selection and setup. This module owns the repeated "look at git
state, decide what to do, then commit/push/pull when safe" workflow.
"""

import threading
import time
from dataclasses import dataclass
from pathlib import Path

from gitmo.config import AppConfig, RepoConfig
from gitmo.git_cli import (
    GitCommandError,
    add_remote,
    ahead_behind,
    commit,
    current_branch,
    fetch,
    has_remote,
    has_commits,
    has_staged_or_unstaged_changes,
    init_repo,
    is_git_repo,
    pull_ff_only,
    push,
    set_remote_url,
    stage_all,
    upstream_branch,
)


AUTOSAVE_MESSAGE = "GitMo autosave"
POLL_INTERVAL_SECONDS = 10
DEBOUNCE_SECONDS = 60


@dataclass
class RepoStatus:
    state: str = "idle"
    detail: str = ""
    last_event_at: float = 0.0
    last_snapshot_at: float = 0.0
    last_synced_mtime: float = 0.0
    last_sync_at: float = 0.0
    observed_mtime: float = 0.0


class SyncEngine:
    def __init__(self, config: AppConfig, logger) -> None:
        self.config = config
        self.logger = logger
        self.repo_statuses: dict[str, RepoStatus] = {}
        self._stop_event = threading.Event()
        self._thread: threading.Thread | None = None
        self._lock = threading.Lock()

    def start(self) -> None:
        if self._thread and self._thread.is_alive():
            return
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run_loop, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        self._stop_event.set()
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=2)

    def status_for(self, repo_name: str) -> RepoStatus:
        with self._lock:
            return self.repo_statuses.setdefault(repo_name, RepoStatus())

    def _set_status(self, repo_name: str, state: str, detail: str) -> None:
        with self._lock:
            status = self.repo_statuses.setdefault(repo_name, RepoStatus())
            status.state = state
            status.detail = detail
        self.logger(repo_name, f"[{state}] {detail}")

    def _run_loop(self) -> None:
        while not self._stop_event.is_set():
            self.run_once()
            self._stop_event.wait(POLL_INTERVAL_SECONDS)

    def run_once(self) -> None:
        for repo_name, repo_config in self.config.repos.items():
            if not repo_config.enabled:
                continue
            repo_path = self.repo_path_for(repo_name, repo_config)
            if not repo_path.exists() or not is_git_repo(repo_path):
                self._set_status(repo_name, "missing", "Local repository is missing.")
                continue
            try:
                self._sync_repo(repo_path, repo_name, repo_config)
            except GitCommandError as exc:
                self._set_status(repo_name, "error", str(exc))
            except Exception as exc:  # pragma: no cover
                self._set_status(repo_name, "error", f"Unexpected error: {exc}")

    def repo_path_for(self, repo_name: str, repo_config: RepoConfig) -> Path:
        if repo_config.local_path:
            return Path(repo_config.local_path).expanduser()
        return Path(self.config.gitmo_path).expanduser() / repo_name

    def _sync_repo(self, repo_path: Path, repo_name: str, repo_config: RepoConfig) -> None:
        status = self.status_for(repo_name)
        latest_mtime = self._latest_mtime(repo_path)
        if latest_mtime > status.observed_mtime:
            status.last_event_at = time.time()
            status.last_snapshot_at = latest_mtime
            status.observed_mtime = latest_mtime

        if repo_config.sync_mode == "two-way":
            self._sync_two_way(repo_path, repo_name)
        else:
            self._sync_one_way(repo_path, repo_name)
        status.last_sync_at = time.time()

        if (
            status.last_event_at
            and time.time() - status.last_event_at >= DEBOUNCE_SECONDS
            and status.last_snapshot_at > status.last_synced_mtime
        ):
            self._autosave(repo_path, repo_name)
            status.last_synced_mtime = status.last_snapshot_at
            status.last_event_at = 0.0

    def _sync_one_way(self, repo_path: Path, repo_name: str) -> None:
        fetch(repo_path)
        ahead, behind = ahead_behind(repo_path)
        if behind > 0:
            self._set_status(
                repo_name,
                "warning",
                f"Remote is ahead by {behind} commit(s); one-way mode will not pull.",
            )
        elif ahead > 0:
            self._set_status(repo_name, "pending", f"Local is ahead by {ahead} commit(s).")
        else:
            self._set_status(repo_name, "idle", "Watching for local changes.")

    def _sync_two_way(self, repo_path: Path, repo_name: str) -> None:
        fetch(repo_path)
        ahead, behind = ahead_behind(repo_path)
        if ahead > 0 and behind > 0:
            self._set_status(
                repo_name,
                "conflict",
                f"Both local and remote changed ({ahead} ahead, {behind} behind).",
            )
            return
        if behind > 0:
            pull_ff_only(repo_path)
            self._set_status(repo_name, "pulled", f"Pulled {behind} remote commit(s).")
        elif ahead > 0:
            self._set_status(repo_name, "pending", f"Local is ahead by {ahead} commit(s).")
        else:
            self._set_status(repo_name, "idle", "Watching for local and remote changes.")

    def _autosave(self, repo_path: Path, repo_name: str) -> None:
        if not has_staged_or_unstaged_changes(repo_path):
            return
        if commit(repo_path, AUTOSAVE_MESSAGE):
            if upstream_branch(repo_path):
                push(repo_path)
            else:
                branch = current_branch(repo_path) or "main"
                push(repo_path, set_upstream=True, branch=branch)
            self._set_status(repo_name, "synced", "Committed and pushed local changes.")

    def prepare_new_remote_repo(self, repo_path: Path, clone_url: str, force: bool = False) -> None:
        if not is_git_repo(repo_path):
            init_repo(repo_path)
        if has_remote(repo_path):
            set_remote_url(repo_path, clone_url)
        else:
            add_remote(repo_path, clone_url)
        if has_staged_or_unstaged_changes(repo_path) or has_commits(repo_path):
            self._initial_push(repo_path, force=force)

    def _initial_push(self, repo_path: Path, force: bool = False) -> None:
        stage_all(repo_path)
        if not has_commits(repo_path):
            commit(repo_path, AUTOSAVE_MESSAGE)
        branch = current_branch(repo_path) or "main"
        if force:
            from gitmo.git_cli import force_push

            force_push(repo_path, branch)
        else:
            push(repo_path, set_upstream=True, branch=branch)

    def _latest_mtime(self, repo_path: Path) -> float:
        latest = 0.0
        for path in repo_path.rglob("*"):
            if ".git" in path.parts:
                continue
            try:
                latest = max(latest, path.stat().st_mtime)
            except FileNotFoundError:
                continue
        return latest
