"""CLI interface for domesday.

Usage:
    domes add -p vbo "Some important caveat."
    domes add -p vbo --file notes.md --author ben
    domes ingest -p vbo ./project-notes/ --author ben
    domes search -p vbo "VBO timestamp issues"
    domes ask -p vbo "What are the known caveats?"
    domes list -p vbo --n 10
    domes projects
    domes stats -p vbo
"""

from __future__ import annotations

import contextlib
import logging
import sys
from pathlib import Path
from typing import Annotated

import rich.status
import typer

from domesday import config
from domesday.core import models

app = typer.Typer(
    help="domesday — a shared knowledge base that keeps your team's AI tools informed.",
)

# Common option definitions reused across the callback and every command.
_project_option = typer.Option(
    "--project", "-p", help="Project to operate on (overrides config default)."
)
_verbose_option = typer.Option("--verbose", "-v", help="Show INFO-level logs.")
_debug_option = typer.Option(
    "--debug", "-d", help="Show DEBUG-level logs (very verbose)."
)
_config_option = typer.Option("--config", help="Path to config TOML file.")


def _status(ctx: typer.Context, message: str):
    """Return a spinner, or a no-op context manager when verbose/debug logging is active."""
    if ctx.obj.get("show_logs"):
        return contextlib.nullcontext()
    return rich.status.Status(message)


def _apply_common_options(
    ctx: typer.Context,
    *,
    project: str | None,
    verbose: bool,
    debug: bool,
    config_path: Path | None,
) -> None:
    """Merge command-level common options into ctx.obj, overriding the callback values."""
    if config_path is not None:
        ctx.obj["config_path"] = config_path
    if project is not None:
        ctx.obj["project"] = project
    if debug:
        logging.getLogger().setLevel(logging.DEBUG)
        ctx.obj["show_logs"] = True
    elif verbose:
        logging.getLogger().setLevel(logging.INFO)
        ctx.obj["show_logs"] = True


@app.callback()
def _callback(
    ctx: typer.Context,
    config_path: Annotated[Path | None, _config_option] = None,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
) -> None:
    """Configure logging and store shared state."""
    if debug:
        level = logging.DEBUG
    elif verbose:
        level = logging.INFO
    else:
        level = logging.WARNING

    logging.basicConfig(
        level=level,
        format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
        datefmt="%H:%M:%S",
    )

    ctx.ensure_object(dict)
    ctx.obj["config_path"] = config_path
    ctx.obj["project"] = project
    ctx.obj["show_logs"] = verbose or debug


# -------------------------------------------------------------------
# init
# -------------------------------------------------------------------

_TEMPLATE_FILENAME = "default_config.toml"


@app.command()
def init(
    ctx: typer.Context,
    output: Annotated[
        Path,
        typer.Option("--output", "-o", help="Output path for the config file."),
    ] = config.DEFAULT_CONFIG_PATH,
    force: Annotated[
        bool,
        typer.Option("--force", "-f", help="Overwrite existing config file."),
    ] = False,
) -> None:
    """Write a default config file."""
    from importlib.resources import files

    if output.exists() and not force:
        typer.echo(
            f"Config file already exists: {output}\nUse --force to overwrite.",
            err=True,
        )
        raise typer.Exit(code=1)

    template = (
        files("domesday").joinpath(_TEMPLATE_FILENAME).read_text(encoding="utf-8")
    )
    output.parent.mkdir(parents=True, exist_ok=True)
    output.write_text(template, encoding="utf-8")
    typer.echo(f"Wrote {output}")


# -------------------------------------------------------------------
# add
# -------------------------------------------------------------------


@app.command()
def add(
    ctx: typer.Context,
    text: Annotated[str | None, typer.Argument(help="Snippet text to add.")] = None,
    file: Annotated[
        Path | None,
        typer.Option("--file", "-f", help="Read snippet from file."),
    ] = None,
    author: Annotated[
        str,
        typer.Option("--author", "-a", help="Who is adding this."),
    ] = "anonymous",
    tags: Annotated[
        str, typer.Option("--tags", "-t", help="Comma-separated tags.")
    ] = "",
    snippet_type: Annotated[
        models.SnippetType,
        typer.Option("--type", help="Snippet type."),
    ] = models.SnippetType.PROSE,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Add a single snippet to the knowledge base."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    if file:
        content = file.read_text(encoding="utf-8")
    elif text:
        content = text
    elif not sys.stdin.isatty():
        content = sys.stdin.read()
    else:
        edited = typer.edit(text="# Enter your snippet below\n")
        if not edited:
            typer.echo("Aborted — no content provided.", err=True)
            return
        content = edited

    tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else []

    with _status(ctx, "Adding snippet..."):
        cfg = config.Config.load(ctx.obj["config_path"])
        proj = ctx.obj["project"] or cfg.default_project
        pipeline = config.build_pipeline(cfg=cfg, project=proj)
        snippet = pipeline.add_snippet(
            content,
            project=proj,
            author=author,
            tags=tag_list,
            snippet_type=snippet_type,
        )
    typer.echo(
        f"✓ Added snippet {snippet.id[:8]} to project '{snippet.project}' "
        f"({len(content)} chars)"
    )


# -------------------------------------------------------------------
# ingest
# -------------------------------------------------------------------


@app.command()
def ingest(
    ctx: typer.Context,
    path: Annotated[Path, typer.Argument(help="File or directory to ingest.")],
    author: Annotated[
        str,
        typer.Option("--author", "-a", help="Who is adding these."),
    ] = "anonymous",
    extensions: Annotated[
        str,
        typer.Option(
            "--extensions", "-e", help="Comma-separated file extensions to include."
        ),
    ] = ".md,.txt,.py,.json,.csv,.yaml,.yml",
    no_recursive: Annotated[
        bool,
        typer.Option("--no-recursive", help="Don't recurse into subdirectories."),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Ingest a file or directory into the knowledge base."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    ext_set = {
        e.strip() if e.strip().startswith(".") else f".{e.strip()}"
        for e in extensions.split(",")
    }

    cfg = config.Config.load(ctx.obj["config_path"])
    proj = ctx.obj["project"] or cfg.default_project
    pipeline = config.build_pipeline(cfg=cfg, project=proj)

    if path.is_file():
        content = path.read_text(encoding="utf-8")
        with _status(ctx, "Ingesting file..."):
            snippet = pipeline.add_snippet(
                content,
                project=proj,
                author=author,
                source_file=str(path),
            )
        typer.echo(
            f"✓ Ingested {path.name} → snippet {snippet.id[:8]} " f"(project: {proj})"
        )
    else:
        with _status(ctx, "Ingesting directory..."):
            result = pipeline.ingest_directory(
                path,
                project=proj,
                author=author,
                extensions=ext_set,
                recursive=not no_recursive,
            )
        typer.echo(
            f"✓ Ingested {result.files_processed} files → "
            f"{result.snippets_created} snippets, "
            f"{result.chunks_created} chunks "
            f"(project: {proj})"
        )
        if result.errors:
            typer.echo(f"⚠ {len(result.errors)} errors:", err=True)
            for err in result.errors:
                typer.echo(f"  - {err}", err=True)


# -------------------------------------------------------------------
# search
# -------------------------------------------------------------------


@app.command()
def search(
    ctx: typer.Context,
    query: Annotated[str, typer.Argument(help="Search query.")],
    n: Annotated[int, typer.Option("--n", "-n", help="Number of results.")] = 5,
    tags: Annotated[
        str,
        typer.Option("--tags", "-t", help="Filter by tags (comma-separated)."),
    ] = "",
    all_projects: Annotated[
        bool,
        typer.Option("--all-projects", help="Search across all projects."),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Semantic search over the knowledge base."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None

    with _status(ctx, "Searching..."):
        cfg = config.Config.load(ctx.obj["config_path"])
        proj = "all" if all_projects else (ctx.obj["project"] or cfg.default_project)
        pipeline = config.build_pipeline(
            cfg=cfg, project=None if all_projects else proj
        )
        results = pipeline.search(
            query,
            project=proj,
            k=n,
            tags=tag_list,
            min_score=pipeline.min_score,
        )

    if not results:
        label = "any project" if all_projects else f"project '{proj}'"
        typer.echo(f"No results found in {label}.")
        return

    for i, r in enumerate(results, 1):
        sid = r.snippet.id[:8]
        score = f"{r.score:.3f}"
        author = r.snippet.author
        date = r.snippet.created_at.strftime("%Y-%m-%d")
        rproj = r.snippet.project
        tags_str = f" [{', '.join(r.snippet.tags)}]" if r.snippet.tags else ""
        typer.echo(f"\n{'─' * 60}")
        typer.echo(
            f"  #{i}  snippet-{sid}  score={score}  "
            f"{author} {date}  project={rproj}{tags_str}"
        )
        typer.echo(f"{'─' * 60}")
        text = r.snippet.raw_text
        if len(text) > 500:
            text = text[:500] + "…"
        typer.echo(text)


# -------------------------------------------------------------------
# ask
# -------------------------------------------------------------------


@app.command()
def ask(
    ctx: typer.Context,
    question: Annotated[str, typer.Argument(help="Question to ask.")],
    n: Annotated[
        int,
        typer.Option("--n", "-n", help="Number of context snippets to retrieve."),
    ] = 8,
    show_sources: Annotated[
        bool,
        typer.Option(
            "--show-sources", "-s", help="Print source snippets after answer."
        ),
    ] = False,
    all_projects: Annotated[
        bool,
        typer.Option("--all-projects", help="Search across all projects."),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Ask a question — retrieves context and generates an answer."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    with _status(ctx, "Thinking..."):
        cfg = config.Config.load(ctx.obj["config_path"])
        proj = "all" if all_projects else (ctx.obj["project"] or cfg.default_project)
        pipeline = config.build_pipeline(
            cfg=cfg, project=None if all_projects else proj
        )
        response = pipeline.ask(
            question, project=proj, k=n, min_score=pipeline.min_score
        )

    typer.echo(f"\n{response.answer}\n")

    if show_sources:
        typer.echo(f"{'─' * 60}")
        typer.echo(f"Sources ({len(response.sources)} snippets retrieved):")
        for r in response.sources:
            sid = r.snippet.id[:8]
            rproj = r.snippet.project
            typer.echo(f"  [{sid}] ({rproj}) {r.snippet.raw_text[:80]}…")


# -------------------------------------------------------------------
# list
# -------------------------------------------------------------------


@app.command("list")
def list_snippets(
    ctx: typer.Context,
    n: Annotated[
        int, typer.Option("--n", "-n", help="Number of snippets to show.")
    ] = 20,
    author: Annotated[
        str | None, typer.Option("--author", "-a", help="Filter by author.")
    ] = None,
    all_projects: Annotated[
        bool,
        typer.Option("--all-projects", help="List across all projects."),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """List recent snippets."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    with _status(ctx, "Loading snippets..."):
        cfg = config.Config.load(ctx.obj["config_path"])
        proj = None if all_projects else (ctx.obj["project"] or cfg.default_project)
        pipeline = config.build_pipeline(cfg=cfg, project=proj)
        snippets = pipeline.snippet_store.list_recent(n, project=proj, author=author)

    if not snippets:
        label = "any project" if all_projects else f"project '{proj}'"
        typer.echo(f"No snippets found in {label}.")
        return

    for s in snippets:
        sid = s.id[:8]
        date = s.created_at.strftime("%Y-%m-%d %H:%M")
        tags_str = f" [{', '.join(s.tags)}]" if s.tags else ""
        preview = s.raw_text[:80].replace("\n", " ")
        typer.echo(
            f"  {sid}  {s.project:12s}  {date}  {s.author:12s}{tags_str}  {preview}…"
        )


# -------------------------------------------------------------------
# projects
# -------------------------------------------------------------------


@app.command()
def projects(
    ctx: typer.Context,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """List all projects in the knowledge base."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    with _status(ctx, "Loading projects..."):
        pipeline = config.build_pipeline(ctx.obj["config_path"])
        # Projects command uses global defaults (no per-project resolution).
        project_list = pipeline.list_projects()

    if not project_list:
        typer.echo("No projects found.")
        return

    typer.echo("Projects:")
    for proj in project_list:
        count = pipeline.snippet_store.count(project=proj)
        typer.echo(f"  {proj:20s}  {count} snippets")


# -------------------------------------------------------------------
# rename-project
# -------------------------------------------------------------------


@app.command("rename-project")
def rename_project(
    ctx: typer.Context,
    old_name: Annotated[str, typer.Argument(help="Current project name.")],
    new_name: Annotated[str, typer.Argument(help="New project name.")],
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Rename a project (updates all snippets and vector metadata)."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    with _status(ctx, "Renaming project..."):
        # Rename uses old project's config to access the right stores.
        pipeline = config.build_pipeline(ctx.obj["config_path"], project=old_name)
        count = pipeline.rename_project(old_name, new_name)
    typer.echo(f"✓ Renamed '{old_name}' → '{new_name}' ({count} snippets)")


# -------------------------------------------------------------------
# stats
# -------------------------------------------------------------------


@app.command()
def stats(
    ctx: typer.Context,
    all_projects: Annotated[
        bool,
        typer.Option("--all-projects", help="Show stats across all projects."),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Show knowledge base statistics."""
    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    with _status(ctx, "Loading stats..."):
        cfg = config.Config.load(ctx.obj["config_path"])
        proj = None if all_projects else (ctx.obj["project"] or cfg.default_project)
        pipeline = config.build_pipeline(cfg=cfg, project=proj)
    if all_projects:
        project_list = pipeline.list_projects()
        total_snippets = 0
        for p in project_list:
            count = pipeline.snippet_store.count(project=p)
            total_snippets += count
            typer.echo(f"  {p:20s}  {count} snippets")
        typer.echo(f"  {'TOTAL':20s}  {total_snippets} snippets")
    else:
        snippet_count = pipeline.snippet_store.count(project=proj)
        typer.echo(f"Project:  {proj}")
        typer.echo(f"Snippets: {snippet_count}")

    vec_count = pipeline.vec_store.count()
    typer.echo(f"Chunks (all projects): {vec_count}")


# -------------------------------------------------------------------
# server
# -------------------------------------------------------------------


@app.command()
def server(
    ctx: typer.Context,
    port: Annotated[
        int,
        typer.Option("--port", help="Port for the web server."),
    ] = 8765,
    no_browser: Annotated[
        bool,
        typer.Option(
            "--no-browser", help="Start the server without opening a browser."
        ),
    ] = False,
    project: Annotated[str | None, _project_option] = None,
    verbose: Annotated[bool, _verbose_option] = False,
    debug: Annotated[bool, _debug_option] = False,
    config_path: Annotated[Path | None, _config_option] = None,
) -> None:
    """Start a local web UI for the knowledge base. Ctrl-C to stop."""
    from domesday.web.server import run_server

    _apply_common_options(
        ctx, project=project, verbose=verbose, debug=debug, config_path=config_path
    )

    cfg = config.Config.load(ctx.obj["config_path"])
    pipeline = config.build_pipeline(cfg=cfg)

    url = f"http://127.0.0.1:{port}"
    typer.echo(f"Starting web server at {url}")
    typer.echo("Ctrl-C to stop.")
    if not no_browser:
        import webbrowser

        webbrowser.open(url)
    run_server(pipeline, port=port)


# -------------------------------------------------------------------
# version
# -------------------------------------------------------------------


@app.command()
def version() -> None:
    """Print the domesday version."""
    import importlib.metadata

    typer.echo(importlib.metadata.version("domesday"))


def main() -> None:
    app()


if __name__ == "__main__":
    main()
