"""CLI for code context operations."""

import json
from pathlib import Path
from typing import Annotated

import typer
import uvicorn

from codectx.api.routes import build_router
from codectx.config import CodeSettings
from codectx.db.repository import CodeRepository
from codectx.indexer import CodeIndexer
from common.server import create_app
from common.version import __version__

app = typer.Typer(help="Code context commands.", no_args_is_help=True)


def build_app(settings: CodeSettings | None = None):
    """Create the codectx ASGI app."""
    resolved_settings = settings or CodeSettings()
    repository = CodeRepository(resolved_settings.db_path)
    repository.create_schema()

    app_instance = create_app("codectx", __version__)
    app_instance.include_router(build_router(repository))
    return app_instance


def build_mcp_server(settings: CodeSettings, host: str = "127.0.0.1", port: int = 8081):
    """Create an MCP server exposing codectx tools."""
    from mcp.server.fastmcp import FastMCP

    repository = CodeRepository(settings.db_path)
    repository.create_schema()
    server = FastMCP(
        name="codectx",
        instructions="Query and update the local code index for entities, dependencies, and call graphs.",
        host=host,
        port=port,
    )

    @server.tool(description="Index a source directory into the codectx SQLite database.")
    def index_directory(path: str = ".") -> dict[str, int]:
        max_bytes = settings.max_file_size_mb * 1024 * 1024 if settings.max_file_size_mb > 0 else 0
        indexer = CodeIndexer(repository, max_file_size_bytes=max_bytes)
        summary = indexer.index_directory(path)
        snapshot = repository.get_stats()
        return {
            "indexed_files": summary.indexed_files,
            "skipped_files": summary.skipped_files,
            "files": snapshot.files,
            "entities": snapshot.entities,
            "dependencies": snapshot.dependencies,
            "relations": snapshot.relations,
        }

    @server.tool(description="Return codectx index statistics.")
    def stats() -> dict[str, int]:
        return repository.get_stats().to_dict()

    @server.tool(description="Find indexed entities by simple or qualified name.")
    def find_entities(name: str) -> list[dict[str, object]]:
        entities = repository.get_entities_by_name(name)
        return [
            {
                "name": entity.name,
                "qualified_name": entity.qualified_name,
                "type": entity.type,
                "file_id": entity.file_id,
                "start_line": entity.start_line,
                "end_line": entity.end_line,
            }
            for entity in entities
        ]

    @server.tool(description="Return the dependency graph for an indexed file path.")
    def dependency_graph(path: str) -> dict[str, object]:
        graph = repository.get_dependency_graph(path)
        if graph is None:
            raise ValueError(f"No indexed file found for {path}")
        return graph

    @server.tool(description="Return outgoing and incoming call edges for an indexed entity.")
    def call_graph(name: str) -> dict[str, object]:
        graph = repository.get_call_graph(name)
        if graph is None:
            raise ValueError(f"No indexed entity found for {name}")
        return graph

    @server.tool(description="Return the documentation string for an entity by name or qualified name.")
    def get_docs(name: str) -> dict[str, object]:
        entities = repository.get_entities_by_name(name)
        if not entities:
            raise ValueError(f"No indexed entity found for {name}")
        entity = entities[0]
        return {
            "name": entity.name,
            "qualified_name": entity.qualified_name,
            "documentation": entity.documentation or "",
            "type": entity.type,
        }

    @server.tool(description="List all indexed files with their languages and metrics.")
    def list_files(limit: int = 100, offset: int = 0) -> list[dict[str, object]]:
        return repository.list_files(limit=limit, offset=offset)

    @server.tool(description="Return all top-level entities in a file as a structured outline.")
    def get_file_outline(file_path: str) -> dict[str, object]:
        entities = repository.get_file_entities(file_path)
        if not entities:
            return {"file": file_path, "entities": []}
        return {"file": file_path, "entities": entities}

    @server.tool(description="Find all call-graph usages (incoming calls) to an entity by name.")
    def find_usages(name: str) -> dict[str, object]:
        graph = repository.get_call_graph(name)
        if graph is None:
            return {"entity": name, "usages": []}
        return {"entity": graph["entity"], "usages": graph["incoming"]}

    @server.tool(description="Full-text search entities by name or qualified name using a substring query.")
    def search_entities(query: str, limit: int = 20) -> list[dict[str, object]]:
        return repository.search_entities(query, limit=limit)

    @server.tool(description="Return the source code lines for an entity by ID.")
    def get_entity_code(entity_id: int, source_root: str = ".") -> dict[str, object]:
        result = repository.get_entity_code(entity_id, source_root=source_root)
        if result is None:
            raise ValueError(f"No entity found for id={entity_id}")
        return result

    @server.tool(description="Return the import/dependency list for a file path.")
    def get_file_imports(file_path: str) -> dict[str, object]:
        imports = repository.get_file_imports(file_path)
        return {"file": file_path, "imports": imports}

    @server.tool(description="Return file metadata, its imports, and all entities in a single graph context response.")
    def get_graph_context(file_path: str) -> dict[str, object]:
        file_row = repository.get_file_by_path(file_path)
        if file_row is None:
            raise ValueError(f"No indexed file found for {file_path}")
        entities = repository.get_file_entities(file_path)
        imports = repository.get_file_imports(file_path)
        dep_graph = repository.get_dependency_graph(file_path)
        return {
            "file": file_path,
            "language": file_row.language,
            "lines_of_code": file_row.lines_of_code,
            "tokens": file_row.tokens,
            "entities": entities,
            "imports": imports,
            "dependencies": dep_graph.get("dependencies", []) if dep_graph else [],
        }

    @server.tool(description="Return a summary of a file including its outline, imports, and stats.")
    def get_file_context(file_path: str) -> dict[str, object]:
        file_row = repository.get_file_by_path(file_path)
        if file_row is None:
            raise ValueError(f"No indexed file found for {file_path}")
        entities = repository.get_file_entities(file_path)
        imports = repository.get_file_imports(file_path)
        return {
            "file": file_path,
            "language": file_row.language,
            "lines_of_code": file_row.lines_of_code,
            "tokens": file_row.tokens,
            "entity_count": len(entities),
            "outline": [
                {"name": e["name"], "type": e["type"], "start_line": e["start_line"], "end_line": e["end_line"]}
                for e in entities
            ],
            "import_count": len(imports),
            "imports": [i["target_path"] for i in imports],
        }

    @server.tool(description="List all indexed code versions with file/entity counts.")
    def list_versions() -> list[dict[str, object]]:
        return repository.list_versions()

    @server.tool(description="Show files changed between an old version and the latest version.")
    def diff_versions(old_version: str) -> list[dict[str, object]]:
        return repository.get_changed_files(old_version)

    return server


def build_mcp_server_entry(
    binary: str,
    transport: str,
    addr: str,
    db_path: str | None,
) -> dict[str, object]:
    """Build a VS Code MCP server config entry for codectx."""
    if transport == "stdio":
        args = ["code", "mcp", "--transport", "stdio"]
        if db_path:
            args.extend(["--db", db_path])
        return {
            "type": "stdio",
            "command": binary,
            "args": args,
        }

    if transport == "streamable-http":
        return {
            "type": "http",
            "url": f"http://{addr}/mcp",
        }

    raise ValueError(f"Unsupported MCP transport: {transport}. Use 'stdio' or 'streamable-http'.")


def write_mcp_config(
    output_path: Path,
    server_name: str,
    entry: dict[str, object],
) -> Path:
    """Write or update a VS Code MCP config file."""
    if output_path.exists():
        try:
            config = json.loads(output_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError:
            config = {}
    else:
        config = {}

    if not isinstance(config, dict):
        config = {}
    servers = config.get("servers")
    if not isinstance(servers, dict):
        servers = {}
    inputs = config.get("inputs")
    if not isinstance(inputs, list):
        inputs = []

    servers[server_name] = entry
    config["servers"] = servers
    config["inputs"] = inputs

    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
    return output_path


def _parse_addr(addr: str) -> tuple[str, int]:
    host, separator, port_text = addr.rpartition(":")
    if not separator or not host or not port_text:
        raise ValueError(f"Address must be in host:port form, got: {addr}")
    return host, int(port_text)


@app.command("index")
def index(
    path: Annotated[str, typer.Argument(help="Directory to index.")] = ".",
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
    max_file_size_mb: int | None = typer.Option(None, "--max-file-size", help="Skip files larger than this many MB."),
    version: str | None = typer.Option(None, "--version", help="Version tag for this indexing run (e.g. v2.5.0)."),
    label: str | None = typer.Option(None, "--label", help="Release label (e.g. 'Sprint 42')."),
) -> None:
    """Index a source tree."""
    settings = CodeSettings()
    repository = CodeRepository(db_path or settings.db_path)
    max_mb = max_file_size_mb if max_file_size_mb is not None else settings.max_file_size_mb

    # Create a version record if --version is specified
    version_id = None
    if version:
        version_id = repository.create_version(
            version_tag=version,
            release_label=label or "",
        )
        typer.echo(f"🏷️  Created version: {version}" + (f" ({label})" if label else ""))

    typer.echo("🚀  Starting Code Indexing...")

    indexer = CodeIndexer(
        repository,
        max_file_size_bytes=max_mb * 1024 * 1024 if max_mb > 0 else 0,
        version_id=version_id,
    )
    files_to_index = indexer.list_files(path)
    total_files = len(files_to_index)

    def _log(msg: str) -> None:
        typer.echo(f"  {msg}")

    _parse_count = [0]

    def _on_parse_progress() -> None:
        _parse_count[0] += 1
        c = _parse_count[0]
        if c % 200 == 0 or c == total_files:
            typer.echo(f"\r  🔄 Parsing files... {c}/{total_files}", nl=False)

    _write_state = {"total": 0, "done": 0}

    def _on_write_total(total: int) -> None:
        _write_state["total"] = total
        typer.echo("")  # newline after parsing
        typer.echo(f"  💾 Writing to database... 0/{total}", nl=False)

    def _on_write_progress(n: int) -> None:
        _write_state["done"] += n
        typer.echo(f"\r  💾 Writing to database... {_write_state['done']}/{_write_state['total']}", nl=False)

    summary = indexer.index_directory(
        path,
        progress_callback=_on_parse_progress,
        files=files_to_index,
        log_callback=_log,
        write_total_callback=_on_write_total,
        write_progress_callback=_on_write_progress,
    )
    typer.echo("")  # final newline

    if version_id:
        repository.update_version_counts(version_id)

    stats_snapshot = repository.get_stats()

    typer.echo("\n✅  Indexing Complete!")
    typer.echo("📊  Summary:")
    typer.echo(f"  📄  Processed/Indexed files: {summary.indexed_files}")
    typer.echo(f"  ⏭️   Unchanged files (skipped): {summary.unchanged_files}")
    typer.echo(f"  ⚠️   Skipped (too large/ignored): {summary.skipped_files}")
    typer.echo("\n📈  Database Statistics:")
    typer.echo(f"  📂  Total Files: {stats_snapshot.files}")
    typer.echo(f"  🧱  Total Entities: {stats_snapshot.entities}")
    typer.echo(f"  🔗  Total Dependencies: {stats_snapshot.dependencies}")
    typer.echo(f"  🕸️   Total Relations: {stats_snapshot.relations}")
    if stats_snapshot.versions:
        typer.echo(f"  🏷️   Versions: {stats_snapshot.versions} (latest: {stats_snapshot.latest_version})")


@app.command("query")
def query(
    query_type: Annotated[str, typer.Argument(help="Query type: entity, deps, calls.")],
    value: Annotated[str, typer.Argument(help="Entity name or file path to query.")],
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
) -> None:
    """Query indexed entities and dependency graphs."""
    settings = CodeSettings()
    repository = CodeRepository(db_path or settings.db_path)
    repository.create_schema()

    if query_type == "entity":
        entities = repository.get_entities_by_name(value)
        typer.echo(f"🔍 Found {len(entities)} entities for '{value}'")
        for entity in entities:
            qualifier = entity.qualified_name or entity.name
            typer.echo(
                f"  🧱 [{entity.type}] {qualifier} (file_id={entity.file_id}, lines={entity.start_line}-{entity.end_line})"
            )
        return

    if query_type == "deps":
        graph = repository.get_dependency_graph(value)
        if graph is None:
            typer.echo(f"❌ No indexed file found for {value}")
            raise typer.Exit(code=1)
        typer.echo(f"📄 File: {graph['file']}")
        typer.echo("🔗 Dependencies:")
        for dependency in graph["dependencies"]:
            typer.echo(f"  - {dependency}")
        return

    if query_type == "calls":
        graph = repository.get_call_graph(value)
        if graph is None:
            typer.echo(f"❌ No indexed entity found for {value}")
            raise typer.Exit(code=1)
        typer.echo(f"🧱 Entity: {graph['entity']}")
        typer.echo("➡️ Outgoing calls:")
        for edge in graph["outgoing"]:
            typer.echo(f"  - {edge['target']} (line {edge['line_number']})")
        typer.echo("⬅️ Incoming calls:")
        for edge in graph["incoming"]:
            typer.echo(f"  - {edge['source']} (line {edge['line_number']})")
        return

    typer.echo(f"❌ Unknown query type: {query_type}")
    raise typer.Exit(code=1)


@app.command("stats")
def stats(
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
    version: str | None = typer.Option(None, "--version", help="Show stats for a specific version."),
) -> None:
    """Show index stats."""
    settings = CodeSettings()
    repository = CodeRepository(db_path or settings.db_path)
    repository.create_schema()
    snapshot = repository.get_stats(version_tag=version)
    typer.echo("📈 Code Context Database Statistics:")
    if version:
        typer.echo(f"  🏷️  Version: {version}")
    typer.echo(f"  📂 Files: {snapshot.files}")
    typer.echo(f"  📝 Lines of code: {snapshot.lines_of_code}")
    typer.echo(f"  🪙 Tokens: {snapshot.tokens}")
    typer.echo(f"  🧱 Entities: {snapshot.entities}")
    typer.echo(f"  🕸️ Relations: {snapshot.relations}")
    typer.echo(f"  🔗 Dependencies: {snapshot.dependencies}")
    if snapshot.versions:
        typer.echo(f"  🏷️  Versions: {snapshot.versions} (latest: {snapshot.latest_version})")


@app.command("versions")
def versions(
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
) -> None:
    """List all indexed code versions."""
    settings = CodeSettings()
    repository = CodeRepository(db_path or settings.db_path)
    repository.create_schema()
    vers = repository.list_versions()
    if not vers:
        typer.echo("No versions found. Use --version flag with 'index' to create versions.")
        return
    typer.echo("🏷️  Code Versions:")
    for v in vers:
        latest = " (latest)" if v["is_latest"] else ""
        label = f" — {v['release_label']}" if v["release_label"] else ""
        typer.echo(f"  {v['version_tag']}{label}{latest}: {v['file_count']} files, {v['entity_count']} entities")


@app.command("diff")
def diff(
    old_version: Annotated[str, typer.Argument(help="Old version tag (e.g. v2.4.0)")],
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
) -> None:
    """Show files changed between an old version and the latest version."""
    settings = CodeSettings()
    repository = CodeRepository(db_path or settings.db_path)
    repository.create_schema()
    changes = repository.get_changed_files(old_version)
    if not changes:
        typer.echo("No changes found (or versions not found).")
        return
    added = [c for c in changes if c["change"] == "added"]
    modified = [c for c in changes if c["change"] == "modified"]
    removed = [c for c in changes if c["change"] == "removed"]
    typer.echo(f"📊 Changes since {old_version}:")
    typer.echo(f"  ➕ Added: {len(added)}")
    typer.echo(f"  ✏️  Modified: {len(modified)}")
    typer.echo(f"  ➖ Removed: {len(removed)}")
    for c in changes[:50]:
        symbol = {"added": "+", "modified": "~", "removed": "-"}[c["change"]]
        typer.echo(f"  [{symbol}] {c['path']}")


@app.command("clean")
def clean(db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database.")) -> None:
    """Delete the codectx database and SQLite journal files if present."""
    settings = CodeSettings()
    target = Path(db_path or settings.db_path)
    removed_any = False
    for suffix in ("", "-wal", "-shm"):
        candidate = Path(str(target) + suffix)
        if candidate.exists():
            candidate.unlink()
            removed_any = True
    if removed_any:
        typer.echo(f"🗑️ Deleted database: {target}")
    else:
        typer.echo(f"ℹ️ No database found at: {target}")


@app.command("serve")
def serve(
    host: str | None = typer.Option(None, "--host", help="Bind host."),
    port: int | None = typer.Option(None, "--port", help="Bind port."),
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
) -> None:
    """Run API/UI server."""
    settings = CodeSettings()
    if db_path is not None:
        settings = settings.model_copy(update={"db_path": db_path})
    uvicorn.run(build_app(settings), host=host or settings.host, port=port or settings.port)


@app.command("mcp")
def mcp(
    transport: str = typer.Option("stdio", "--transport", help="Transport: stdio or streamable-http."),
    addr: str = typer.Option("127.0.0.1:8081", "--addr", help="Bind address for HTTP MCP transports."),
    db_path: str | None = typer.Option(None, "--db", help="Path to the codectx SQLite database."),
) -> None:
    """Run the codectx MCP server."""
    settings = CodeSettings()
    if db_path is not None:
        settings = settings.model_copy(update={"db_path": Path(db_path)})

    host, port = _parse_addr(addr)
    server = build_mcp_server(settings, host=host, port=port)
    server.run(transport=transport)


@app.command("setup")
def setup(
    binary: str = typer.Option("ossCtx", "--binary", help="Command used to launch the unified CLI."),
    transport: str = typer.Option("stdio", "--transport", help="Transport: stdio, sse, or streamable-http."),
    addr: str = typer.Option("127.0.0.1:8081", "--addr", help="Address for HTTP MCP transports."),
    db_path: str | None = typer.Option(None, "--db", help="Optional codectx database path to bake into the config."),
    output: str = typer.Option(".vscode/mcp.json", "--output", help="Path to the VS Code MCP config file to write."),
    server_name: str = typer.Option("ossctx-codectx", "--server-name", help="Server name in the MCP config."),
) -> None:
    """Write or update a VS Code MCP config file for codectx."""
    entry = build_mcp_server_entry(binary=binary, transport=transport, addr=addr, db_path=db_path)
    target = write_mcp_config(Path(output), server_name=server_name, entry=entry)
    typer.echo(f"Wrote MCP config: {target}")


def compat_main() -> None:
    """Compatibility entrypoint for legacy `codecontext` command."""
    app()
