"""CLI client that operates against a remote OKB MCP server."""

from __future__ import annotations

import sys

import click

from .config import config
from .mcp_client import OKBClient


def _get_client(server_name: str | None = None) -> OKBClient:
    """Get an MCP client from config (env vars > config file)."""
    try:
        sc = config.get_server(server_name)
    except ValueError as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)
    if not sc.url:
        click.echo(
            f"No URL configured for server '{sc.name}'. "
            "Set url in servers: config or OKB_SERVER_URL.",
            err=True,
        )
        sys.exit(1)
    if not sc.token:
        click.echo(
            f"No token configured for server '{sc.name}'. "
            "Set token in servers: config or OKB_TOKEN.",
            err=True,
        )
        sys.exit(1)
    return OKBClient(sc.url, sc.token)


def _unwrap_exception(e: BaseException) -> str:
    """Extract a readable message from an exception, unwrapping ExceptionGroups."""
    if isinstance(e, BaseExceptionGroup):
        msgs = [_unwrap_exception(sub) for sub in e.exceptions]
        return "; ".join(msgs)
    return str(e)


def _call(name: str, arguments: dict | None = None) -> None:
    """Call a tool and print the result."""
    ctx = click.get_current_context()
    server_name = ctx.obj.get("server") if ctx.obj else None
    client = _get_client(server_name)
    try:
        result = client.call_tool_sync(name, arguments)
        click.echo(result)
    except SystemExit:
        raise
    except BaseExceptionGroup as eg:
        click.echo(f"Error: {_unwrap_exception(eg)}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@click.group()
@click.version_option(package_name="okb")
@click.option("-s", "--server", default=None, help="Named server from config")
@click.pass_context
def main(ctx, server):
    """OKB client - query a remote knowledge base via MCP."""
    ctx.ensure_object(dict)
    ctx.obj["server"] = server


# ---------------------------------------------------------------------------
# Search commands
# ---------------------------------------------------------------------------


@main.command()
@click.argument("query")
@click.option("-n", "--limit", default=5, type=int, help="Max results")
@click.option("--type", "source_type", default=None, help="Filter by source type")
@click.option("--since", default=None, help="Time filter (e.g. 7d, 30d, 6mo)")
def search(query: str, limit: int, source_type: str | None, since: str | None):
    """Semantic + keyword hybrid search."""
    args: dict = {"query": query, "limit": limit}
    if source_type:
        args["source_type"] = source_type
    if since:
        args["since"] = since
    _call("hybrid_search", args)


@main.command()
@click.argument("query")
@click.option("-n", "--limit", default=5, type=int, help="Max results")
@click.option("--since", default=None, help="Time filter")
def keyword(query: str, limit: int, since: str | None):
    """Full-text keyword search."""
    args: dict = {"query": query, "limit": limit}
    if since:
        args["since"] = since
    _call("keyword_search", args)


# ---------------------------------------------------------------------------
# Document commands
# ---------------------------------------------------------------------------


@main.command()
@click.argument("path")
def get(path: str):
    """Retrieve a document by source path."""
    _call("get_document", {"source_path": path})


@main.command()
@click.option("-n", "--limit", default=10, type=int, help="Max results")
def recent(limit: int):
    """Show recently indexed documents."""
    _call("recent_documents", {"limit": limit})


@main.command()
def sources():
    """Show indexed document statistics."""
    _call("list_sources")


@main.command()
def projects():
    """List known projects."""
    _call("list_projects")


@main.command()
def info():
    """Show database info and statistics."""
    _call("get_database_info")


# ---------------------------------------------------------------------------
# Write commands
# ---------------------------------------------------------------------------


@main.command()
@click.argument("title")
@click.option("--tags", default=None, help="Comma-separated tags")
@click.option("--project", default=None, help="Project name")
def save(title: str, tags: str | None, project: str | None):
    """Save knowledge from stdin.

    Example: echo 'content' | okb save 'My Title' --tags t1,t2
    """
    if sys.stdin.isatty():
        click.echo(
            "Error: Pipe content via stdin. Example: echo 'text' | okb save 'Title'",
            err=True,
        )
        sys.exit(1)
    content = sys.stdin.read().strip()
    if not content:
        click.echo("Error: No content received on stdin.", err=True)
        sys.exit(1)
    args: dict = {"title": title, "content": content}
    if tags:
        args["tags"] = [t.strip() for t in tags.split(",")]
    if project:
        args["project"] = project
    _call("save_knowledge", args)


@main.command()
@click.argument("title")
@click.option("--due", "due_date", default=None, help="Due date (YYYY-MM-DD)")
@click.option("--priority", default=None, type=int, help="Priority 1-5 (1=highest)")
@click.option("--project", default=None, help="Project name")
@click.option("--tags", default=None, help="Comma-separated tags")
def todo(title: str, due_date: str | None, priority: int | None, project: str | None,
         tags: str | None):
    """Create a TODO item."""
    args: dict = {"title": title}
    if due_date:
        args["due_date"] = due_date
    if priority:
        args["priority"] = priority
    if project:
        args["project"] = project
    if tags:
        args["tags"] = [t.strip() for t in tags.split(",")]
    _call("add_todo", args)


# ---------------------------------------------------------------------------
# Sync / rescan
# ---------------------------------------------------------------------------


@main.command()
def rescan():
    """Trigger rescan for file changes."""
    _call("trigger_rescan")


@main.command()
@click.argument("paths", nargs=-1, required=True)
@click.option("--metadata", "-m", default="{}", help="JSON metadata to attach")
@click.option("--project", default=None, help="Project name for all documents")
def ingest(paths: tuple[str, ...], metadata: str, project: str | None):
    """Ingest local files into the remote knowledge base.

    Parses files locally, sends to server for chunking and embedding.

    Example: okb ingest ~/notes/ --project personal
    """
    import json as json_module
    from pathlib import Path

    from .config import config as app_config
    from .ingest import (
        check_file_skip,
        collect_documents,
        is_text_file,
        is_url,
        parse_document,
        parse_url,
    )

    try:
        extra_metadata = json_module.loads(metadata)
    except json_module.JSONDecodeError as e:
        click.echo(f"Error parsing metadata JSON: {e}", err=True)
        sys.exit(1)

    if project:
        extra_metadata["project"] = project

    documents = []
    for path_str in paths:
        if is_url(path_str):
            click.echo(f"Fetching: {path_str}")
            try:
                documents.append(parse_url(path_str, extra_metadata))
            except Exception as e:
                click.echo(f"Error fetching URL: {e}", err=True)
            continue

        path = Path(path_str).resolve()
        if path.is_dir():
            documents.extend(collect_documents(path, extra_metadata))
        elif path.is_file():
            skip_check = check_file_skip(path)
            if skip_check.should_skip:
                prefix = "BLOCKED" if skip_check.is_security else "Skipping"
                click.echo(f"{prefix}: {path} ({skip_check.reason})", err=True)
                continue

            if path.suffix in app_config.all_extensions or path.suffix in (
                ".pdf", ".docx",
            ):
                try:
                    documents.extend(parse_document(path, extra_metadata))
                except ValueError as e:
                    click.echo(f"Skipping: {e}", err=True)
                    continue
            elif is_text_file(path):
                click.echo(f"Parsing as text: {path}")
                documents.extend(
                    parse_document(path, extra_metadata, force=True)
                )
            else:
                click.echo(f"Skipping binary file: {path}", err=True)
        else:
            click.echo(f"Not found: {path_str}", err=True)

    if not documents:
        click.echo("No documents found to ingest.")
        return

    click.echo(f"Found {len(documents)} document(s), sending to server...")

    serialized = [doc.to_dict() for doc in documents]
    batch_size = 20
    ctx = click.get_current_context()
    server_name = ctx.obj.get("server") if ctx.obj else None
    client = _get_client(server_name)

    total_sent = 0
    try:
        for i in range(0, len(serialized), batch_size):
            batch = serialized[i : i + batch_size]
            result = client.call_tool_sync(
                "ingest_documents", {"documents": batch}
            )
            total_sent += len(batch)
            click.echo(result)
    except BaseExceptionGroup as eg:
        click.echo(f"Error: {_unwrap_exception(eg)}", err=True)
        click.echo(
            f"  ({total_sent}/{len(documents)} sent before error)", err=True
        )
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        click.echo(
            f"  ({total_sent}/{len(documents)} sent before error)", err=True
        )
        sys.exit(1)

    click.echo(f"Done! {total_sent} document(s) ingested.")


@main.group()
def sync():
    """Sync operations."""
    pass


@sync.group("run", invoke_without_command=True)
@click.option("--all", "sync_all", is_flag=True, help="Sync all enabled sources")
@click.option("--full", is_flag=True, help="Full sync (ignore incremental state)")
@click.pass_context
def sync_run(ctx, sync_all: bool, full: bool):
    """Trigger sync for API sources.

    Use a subcommand for source-specific options:

    \b
      okb sync run --all           Sync all enabled sources
      okb sync run github [OPTS]   GitHub with --repo, --issues, etc.
      okb sync run todoist         Todoist tasks
      okb sync run dropbox-paper   Dropbox Paper docs
      okb sync run slack           Slack messages
    """
    ctx.ensure_object(dict)
    ctx.obj["full"] = full
    if ctx.invoked_subcommand is None:
        if sync_all:
            args: dict = {"all": True}
            if full:
                args["full"] = True
            _call("trigger_sync", args)
        else:
            click.echo(ctx.get_help())


@sync_run.command("github")
@click.option("--repo", multiple=True, required=True, help="Repo to sync (owner/repo, repeatable)")
@click.option("--branch", default=None, help="Git branch (default: repo default)")
@click.option("--issues", "include_issues", is_flag=True, help="Include issues")
@click.option("--prs", "include_prs", is_flag=True, help="Include pull requests")
@click.option("--wiki", "include_wiki", is_flag=True, help="Include wiki pages")
@click.option("--source", "include_source", is_flag=True, help="All source files (not just docs)")
@click.pass_context
def sync_run_github(
    ctx, repo: tuple[str, ...], branch: str | None,
    include_issues: bool, include_prs: bool, include_wiki: bool, include_source: bool,
):
    """Sync GitHub repositories."""
    args: dict = {"sources": ["github"], "repos": list(repo)}
    if ctx.obj.get("full"):
        args["full"] = True
    if branch:
        args["branch"] = branch
    if include_issues:
        args["include_issues"] = True
    if include_prs:
        args["include_prs"] = True
    if include_wiki:
        args["include_wiki"] = True
    if include_source:
        args["include_source"] = True
    _call("trigger_sync", args)


@sync_run.command("todoist")
@click.pass_context
def sync_run_todoist(ctx):
    """Sync Todoist tasks."""
    args: dict = {"sources": ["todoist"]}
    if ctx.obj.get("full"):
        args["full"] = True
    _call("trigger_sync", args)


@sync_run.command("dropbox-paper")
@click.option("--folder", multiple=True, help="Filter to folder path (repeatable)")
@click.option("--doc", "doc_ids", multiple=True, help="Sync specific document ID (repeatable)")
@click.pass_context
def sync_run_dropbox_paper(ctx, folder: tuple[str, ...], doc_ids: tuple[str, ...]):
    """Sync Dropbox Paper documents."""
    args: dict = {"sources": ["dropbox-paper"]}
    if ctx.obj.get("full"):
        args["full"] = True
    if folder:
        args["folders"] = list(folder)
    if doc_ids:
        args["doc_ids"] = list(doc_ids)
    _call("trigger_sync", args)


@sync_run.command("slack")
@click.option("--channel", multiple=True, help="Channel ID to sync (repeatable)")
@click.pass_context
def sync_run_slack(ctx, channel: tuple[str, ...]):
    """Sync Slack messages."""
    args: dict = {"sources": ["slack"]}
    if ctx.obj.get("full"):
        args["full"] = True
    if channel:
        args["channels"] = list(channel)
    _call("trigger_sync", args)


@sync.command("status")
def sync_status():
    """Show sync source status."""
    _call("list_sync_sources")


# ---------------------------------------------------------------------------
# Synthesis
# ---------------------------------------------------------------------------


@main.group()
def synthesize():
    """Knowledge synthesis operations."""
    pass


@synthesize.command("run")
@click.option("--project", default=None, help="Scope to project")
@click.option("--max-proposals", default=10, type=int, help="Max proposals to generate")
def synthesize_run(project: str | None, max_proposals: int):
    """Generate synthesis proposals."""
    args: dict = {"max_proposals": max_proposals}
    if project:
        args["project"] = project
    _call("synthesize_knowledge", args)


@synthesize.command("pending")
@click.option("--project", default=None, help="Filter by project")
def synthesize_pending(project: str | None):
    """List pending synthesis proposals."""
    args: dict = {}
    if project:
        args["project"] = project
    _call("list_pending_synthesis", args)


@synthesize.command("approve")
@click.argument("pending_id", type=int)
def synthesize_approve(pending_id: int):
    """Approve a synthesis proposal."""
    _call("approve_synthesis", {"pending_id": pending_id})


@synthesize.command("reject")
@click.argument("pending_id", type=int)
def synthesize_reject(pending_id: int):
    """Reject a synthesis proposal."""
    _call("reject_synthesis", {"pending_id": pending_id})


@synthesize.command("analyze")
@click.option("--project", default=None, help="Analyze specific project")
def synthesize_analyze(project: str | None):
    """Analyze knowledge base content."""
    args: dict = {}
    if project:
        args["project"] = project
    _call("analyze_knowledge_base", args)


# ---------------------------------------------------------------------------
# Snapshots
# ---------------------------------------------------------------------------


@main.group()
def snapshot():
    """Snapshot backup/restore."""
    pass


@snapshot.command("save")
@click.argument("name", required=False)
def snapshot_save(name: str | None):
    """Create a database snapshot."""
    args: dict = {}
    if name:
        args["name"] = name
    _call("save_snapshot", args)


@snapshot.command("list")
def snapshot_list():
    """List available snapshots."""
    _call("list_snapshots")


def entry_point():
    """Entry point for the okb client CLI."""
    from .config import InsecureConfigError

    try:
        main()
    except InsecureConfigError:
        sys.exit(1)


if __name__ == "__main__":
    entry_point()
