"""Watch command for file syncing."""

import json
import sys
import threading
import time
from pathlib import Path
from typing import Annotated, Optional

import typer
from rich.progress import (
    Progress,
    SpinnerColumn,
    TextColumn,
    BarColumn,
    MofNCompleteColumn,
    TimeElapsedColumn,
)

from nogic.config import Config, is_dev_mode, get_api_url
from nogic.api.client import get_directory_hash, NogicClient
from nogic.ignore import build_ignore_matcher
from nogic.watcher import FileMonitor, SyncService
from nogic import ui


def _emit_json(event: str, **kwargs):
    """Emit a single NDJSON line to stdout."""
    payload = {"event": event, "timestamp": int(time.time()), **kwargs}
    sys.stdout.write(json.dumps(payload) + "\n")
    sys.stdout.flush()


REPAIR_STAGE_LABELS = {
    "imports": "Resolving imports",
    "nodes": "Computing nodes",
    "call_graph": "Building call graph",
    "edges": "Linking relationships",
    "deleting": "Deleting old edges",
    "creating": "Creating edges",
    "embedding": "Generating embeddings",
    "saving": "Saving embeddings",
}


def watch(
    directory: Annotated[Path, typer.Argument(help="Path to the directory to watch.")] = Path("."),
    ignore: Annotated[Optional[list[str]], typer.Option("--ignore", help="Patterns to ignore.")] = None,
    format: Annotated[Optional[str], typer.Option("--format", help="Output format: text or json.")] = None,
):
    """Watch a directory for file changes and sync to backend."""
    directory = directory.resolve()
    ignore = ignore or []
    json_mode = format == "json"

    config = Config.load(directory)

    if not config.project_id:
        if json_mode:
            _emit_json("error", message="Not a Nogic project. Run `nogic init` to initialize.")
        else:
            ui.error("Not a Nogic project.")
            ui.dim("Run `nogic init` to initialize your project.")
        raise typer.Exit(1)

    # Migrate directory hash to machine-specific format if needed
    if config.project_id and config.api_key:
        new_hash = get_directory_hash(str(directory))
        if config.directory_hash != new_hash:
            try:
                migrate_client = NogicClient(config)
                migrate_client.update_project(config.project_id, directory_hash=new_hash)
                migrate_client.close()
                config.directory_hash = new_hash
                config._save_project_mapping(directory)
            except Exception:
                pass  # Non-fatal, will retry next run

    if not config.api_key:
        if json_mode:
            _emit_json("error", message="Not logged in. Run `nogic login` to authenticate.")
        else:
            ui.error("Not logged in.")
            ui.dim("Run `nogic login` to authenticate.")
        raise typer.Exit(1)

    if not config.project_id:
        if json_mode:
            _emit_json("error", message="No project configured. Run `nogic init` to initialize.")
        else:
            ui.error("No project configured.")
            ui.dim("Run `nogic init` to initialize your project.")
        raise typer.Exit(1)

    config._save_project_mapping(directory)

    if not json_mode:
        if is_dev_mode():
            ui.dev_banner(get_api_url())
        ui.banner("nogic watch", str(directory))
        ui.kv("Project", f"{config.project_id[:8]}...")

    log_fn = (lambda msg: None) if json_mode else (lambda msg: ui.dim(f"  {msg}"))
    sync_service = SyncService(config, directory, log=log_fn, json_mode=json_mode)

    should_ignore = build_ignore_matcher(directory, extra_patterns=ignore)

    # Initial scan
    try:
        sync_service.initial_scan(directory, should_ignore)
        if json_mode:
            files_indexed = len(sync_service._file_cache)
            _emit_json("initial_scan_complete", files_indexed=files_indexed)
    except KeyboardInterrupt:
        if not json_mode:
            ui.console.print()
            ui.dim("Interrupted during initial scan. Cleaning up...")
        try:
            sync_service.client.clear_staging(config.project_id)
        except Exception:
            pass
        sync_service.close()
        raise typer.Exit(1)

    # Quick repair status check (< 200ms) — non-blocking
    repair_needed = False
    repair_embeddings_count = 0
    repair_edges = False
    try:
        status = sync_service.client.get_repair_status(config.project_id)
        repair_embeddings_count = status.get("missing_embeddings", 0)
        repair_edges = status.get("edges_needs_repair", False)
        repair_needed = repair_embeddings_count > 0 or repair_edges
    except Exception as e:
        if json_mode:
            _emit_json("repair_error", message=str(e)[:120])
        else:
            ui.dim(f"  Repair check skipped: {str(e)[:120]}")

    def on_change(path: Path):
        try:
            rel = path.relative_to(directory)
        except ValueError:
            return
        try:
            if sync_service.sync_file_immediate(path):
                if json_mode:
                    _emit_json("synced", path=str(rel))
                else:
                    ui.console.print(f"  [green]SYNCED[/] {rel}")
        except Exception as e:
            err_msg = str(e)
            if "413" in err_msg:
                if json_mode:
                    _emit_json("skip", path=str(rel), reason="file too large")
                else:
                    ui.console.print(f"  [yellow]SKIP[/] {rel} (file too large for API)")
            elif "503" in err_msg or "502" in err_msg:
                if json_mode:
                    _emit_json("error", path=str(rel), message="backend unavailable")
                else:
                    ui.console.print(f"  [red]ERROR[/] {rel} (backend unavailable, will sync on next change)")
            else:
                if json_mode:
                    _emit_json("error", path=str(rel), message=err_msg[:120])
                else:
                    ui.console.print(f"  [red]ERROR[/] {rel}: {err_msg[:120]}")

    def on_delete(path: Path):
        try:
            rel = path.relative_to(directory)
        except ValueError:
            return
        try:
            if sync_service.delete_file_immediate(path):
                if json_mode:
                    _emit_json("deleted", path=str(rel))
                else:
                    ui.console.print(f"  [red]DELETED[/] {rel}")
            else:
                if json_mode:
                    _emit_json("deleted", path=str(rel))
                else:
                    ui.console.print(f"  [dim]DELETED[/] {rel} (not indexed)")
        except Exception as e:
            if json_mode:
                _emit_json("error", path=str(rel), message=str(e)[:80])
            else:
                ui.console.print(f"  [red]DELETED[/] {rel} (error: {str(e)[:80]})")

    monitor = FileMonitor(
        root_path=directory,
        on_change=on_change,
        on_delete=on_delete,
        should_ignore=should_ignore,
    )

    if json_mode:
        _emit_json("ready", message="Watching for changes...")
    else:
        ui.console.print()
        ui.info("Watching for changes... (Ctrl+C to stop)")
        ui.console.print()

    monitor.start()

    # Spawn background repair thread if needed
    if repair_needed:
        parts = []
        if repair_embeddings_count > 0:
            parts.append(f"{repair_embeddings_count} embeddings")
        if repair_edges:
            parts.append("edges")
        repair_desc = " + ".join(parts)

        if json_mode:
            _emit_json("repair_start", missing_embeddings=repair_embeddings_count, edges=repair_edges)
        else:
            ui.warn(f"Repairing {repair_desc} in background...")

        def _background_repair():
            """Run repair in a daemon thread with its own client + progress bar."""
            repair_sync = SyncService(config, directory, log=lambda msg: None, json_mode=False)
            repaired_parts = []

            try:
                progress = Progress(
                    SpinnerColumn(),
                    TextColumn("[progress.description]{task.description}"),
                    BarColumn(),
                    MofNCompleteColumn(),
                    TimeElapsedColumn(),
                    console=ui.console,
                    transient=True,
                )
                progress.start()

                # --- Edge repair ---
                if repair_edges:
                    task = progress.add_task("[cyan]Scanning files", total=None)

                    files_info = repair_sync.scan_files(directory, should_ignore, quiet=True)
                    if not files_info:
                        progress.update(task, description="[red]No files found")
                        progress.stop()
                        return

                    upload_files = [
                        {"path": f["path"], "content": f["content"], "language": f["language"], "hash": ""}
                        for f in files_info
                    ]
                    batches = repair_sync._split_into_batches(upload_files)

                    try:
                        repair_sync.client.clear_staging(config.project_id)
                    except Exception:
                        pass

                    progress.update(task, description=f"[cyan]Uploading {len(files_info)} files", total=len(batches), completed=0)
                    for i, batch in enumerate(batches):
                        repair_sync.client.upload_batch(config.project_id, batch)
                        progress.update(task, completed=i + 1)

                    # Consume edge repair SSE — update progress bar per stage
                    progress.update(task, description="[cyan]Rebuilding edges", total=None, completed=0)
                    for event in repair_sync.client.repair_edges_stream(config.project_id):
                        stage = event.stage
                        if stage == "error":
                            progress.update(task, description=f"[red]{event.message}")
                            break
                        if stage == "complete":
                            repaired_parts.append("edges")
                            progress.update(task, description="[green]Edges repaired", total=1, completed=1)
                            break
                        label = REPAIR_STAGE_LABELS.get(stage, stage)
                        if event.total and event.total > 0:
                            progress.update(task, description=f"[cyan]{label}", total=event.total, completed=event.current)
                        else:
                            progress.update(task, description=f"[cyan]{label}")

                    progress.remove_task(task)

                # --- Embedding repair ---
                if repair_embeddings_count > 0:
                    emb_total = repair_embeddings_count
                    emb_task = progress.add_task(
                        "[cyan]Repairing embeddings",
                        total=emb_total,
                    )
                    for event in repair_sync.client.repair_embeddings_stream(config.project_id):
                        if event.stage == "embedding" and event.total > 0:
                            emb_total = event.total
                            progress.update(emb_task, total=emb_total, completed=event.current)
                        elif event.stage == "saving":
                            progress.update(emb_task, description="[cyan]Saving embeddings")
                        elif event.stage == "complete":
                            progress.update(emb_task, description="[green]Embeddings repaired", completed=emb_total)
                            repaired_parts.append(f"{repair_embeddings_count} embeddings")
                            break

                    progress.remove_task(emb_task)

                progress.stop()

                # Print final result
                desc = ", ".join(repaired_parts) if repaired_parts else "nothing"
                if json_mode:
                    _emit_json("repair_complete", repaired=desc)
                else:
                    ui.success(f"Repaired {desc}.")

            except Exception as e:
                try:
                    progress.stop()
                except Exception:
                    pass
                if json_mode:
                    _emit_json("repair_error", message=str(e)[:120])
                else:
                    ui.dim(f"  Background repair failed: {str(e)[:120]}")
            finally:
                repair_sync.close()

        repair_thread = threading.Thread(target=_background_repair, daemon=True)
        repair_thread.start()

    try:
        while monitor.is_alive():
            time.sleep(1)
    except KeyboardInterrupt:
        if not json_mode:
            ui.console.print()
            ui.dim("Stopping...")
    finally:
        monitor.stop()
        sync_service.close()

    if not json_mode:
        ui.success("Done.")
