from __future__ import annotations

import json
from pathlib import Path
from typing import Callable

import typer

from simplicity_cli.client import SimplicityApiClient
from simplicity_cli.config import resolve_runtime_config, save_api_key
from simplicity_cli.errors import CliError, ExitCode
from simplicity_cli.models import GlobalOptions
from simplicity_cli.output import OutputReporter
from simplicity_cli.workflows import (
    load_optional_text,
    run_fill_existing,
    run_fill_new,
    run_task_status,
    run_task_wait,
)

app = typer.Typer(
    name="simplicity-cli",
    help=(
        "CLI for AI-assisted PDF form filling.\n\n"
        "Auth: pass --api-key or set SIMPLICITY_AI_API_KEY.\n"
        "Pass --api-key once to save it for future runs.\n"
        "Primary commands: new, existing, status, wait.\n"
        "Run `simplicity-cli help` for a one-page guide."
    ),
    no_args_is_help=False,
    add_completion=False,
)
legacy_fill_app = typer.Typer(name="fill", no_args_is_help=True)
legacy_task_app = typer.Typer(name="task", no_args_is_help=True)
app.add_typer(legacy_fill_app, name="fill", hidden=True)
app.add_typer(legacy_task_app, name="task", hidden=True)

FULL_HELP_TEXT = """simplicity-cli one-page help

Auth:
  --api-key <KEY>  (or SIMPLICITY_AI_API_KEY env var)
  Run `simplicity-cli --api-key <KEY>` once to save login locally.

Primary commands:
  simplicity-cli new ...
  simplicity-cli existing FORM_ID ...
  simplicity-cli status TASK_ID
  simplicity-cli wait TASK_ID

Command: new
  simplicity-cli new (--form-file PATH | --form-url URL) [options]
  Rules:
    - Exactly one of --form-file/--form-url is required.
    - Provide at least one source via:
      --source-file/--source-url OR --context/--context-file
    - --context and --context-file are mutually exclusive.
    - --instructions and --instructions-file are mutually exclusive.
    - --output cannot be used with --no-download.
  Common examples:
    simplicity-cli new --form-file ./form.pdf --context "Applicant: Jane Doe"
    simplicity-cli new --form-file ./form.pdf --source-file ./w2.pdf --source-file ./id.pdf
    simplicity-cli new --form-url https://example.com/form.pdf --source-url https://example.com/source.pdf --no-wait --no-download

Command: existing
  simplicity-cli existing FORM_ID [options]
  Common examples:
    simplicity-cli existing FORM_ID --context "Use latest profile data"
    simplicity-cli existing FORM_ID --context-file ./context.txt --output ./filled.pdf
    simplicity-cli existing FORM_ID --no-wait --no-download

Command: status
  simplicity-cli status TASK_ID

Command: wait
  simplicity-cli wait TASK_ID [--poll-interval-seconds 2] [--max-wait-seconds 1800]

JSON output:
  Add --json to any command for one machine-readable JSON object.

Legacy aliases (still supported):
  simplicity-cli fill new ...
  simplicity-cli fill existing ...
  simplicity-cli task status ...
  simplicity-cli task wait ...
"""


@app.callback(invoke_without_command=True)
def root_callback(
    ctx: typer.Context,
    api_key: str | None = typer.Option(
        None,
        "--api-key",
        help="API key for X-API-Key auth. Overrides SIMPLICITY_AI_API_KEY.",
    ),
    json_output: bool = typer.Option(False, "--json", help="Emit strict JSON output."),
    request_timeout_seconds: float = typer.Option(
        60.0,
        "--request-timeout-seconds",
        min=0.001,
        help="Per-request timeout in seconds.",
    ),
) -> None:
    ctx.obj = GlobalOptions(
        api_key=api_key,
        json_output=json_output,
        request_timeout_seconds=request_timeout_seconds,
    )

    if api_key:
        try:
            config_path = save_api_key(api_key)
        except CliError as exc:
            raise _as_typer_exit(ctx, exc, command="login") from exc

        if ctx.invoked_subcommand is None:
            if json_output:
                typer.echo(
                    json.dumps(
                        {
                            "command": "login",
                            "ok": True,
                            "status": "saved",
                            "config_path": str(config_path),
                        },
                        ensure_ascii=True,
                    )
                )
            else:
                typer.echo(f"API key saved to {config_path}")
            raise typer.Exit(code=0)

    if ctx.invoked_subcommand is None:
        typer.echo(ctx.get_help())
        raise typer.Exit(code=0)


@app.command("help", help="Print a complete one-page command guide.")
def help_command() -> None:
    typer.echo(FULL_HELP_TEXT)


@app.command(
    "new",
    help=(
        "Create and autofill a new form from a PDF file or URL.\n\n"
        "Rules:\n"
        "- Provide exactly one of --form-file or --form-url.\n"
        "- Provide at least one source: --source-file/--source-url OR --context/--context-file.\n"
        "- Use either --context or --context-file (not both).\n"
        "- Use either --instructions or --instructions-file (not both).\n"
        "- --output cannot be combined with --no-download.\n\n"
        "Examples:\n"
        "  simplicity-cli new --form-file ./form.pdf --context \"Applicant: Jane Doe\"\n"
        "  simplicity-cli new --form-file ./form.pdf --source-file ./w2.pdf --source-file ./id.pdf\n"
        "  simplicity-cli new --form-url https://example.com/form.pdf --source-url https://example.com/source.pdf --no-wait --no-download"
    ),
)
def new_command(
    ctx: typer.Context,
    form_file: Path | None = typer.Option(
        None,
        "--form-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Local path to the PDF form.",
    ),
    form_url: str | None = typer.Option(None, "--form-url", help="URL to the form file."),
    source_file: list[Path] = typer.Option(
        [],
        "--source-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Source file path(s) used for autofill context.",
    ),
    source_url: list[str] = typer.Option(
        [],
        "--source-url",
        help="Source file URL(s) used for autofill context.",
    ),
    context: str | None = typer.Option(None, "--context", help="Free-text context for autofill."),
    context_file: Path | None = typer.Option(
        None,
        "--context-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with autofill context.",
    ),
    instructions: str | None = typer.Option(None, "--instructions", help="AI instructions override."),
    instructions_file: Path | None = typer.Option(
        None,
        "--instructions-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with AI instructions.",
    ),
    no_wait: bool = typer.Option(False, "--no-wait", help="Do not wait for autofill completion."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
    no_download: bool = typer.Option(False, "--no-download", help="Do not download completed form."),
    output: Path | None = typer.Option(
        None,
        "--output",
        help="Output file path for downloaded PDF.",
    ),
) -> None:
    context_text = load_optional_text(context, context_file, "context")
    instructions_text = load_optional_text(instructions, instructions_file, "instructions")

    if (form_file is None) == (form_url is None):
        raise _as_typer_exit(
            ctx,
            CliError(
                "provide exactly one of --form-file or --form-url.",
                exit_code=ExitCode.USAGE_ERROR,
                code="invalid_input",
            ),
            command="fill new",
        )
    if not source_file and not source_url and not context_text:
        raise _as_typer_exit(
            ctx,
            CliError(
                "fill new requires at least one source input (--source-file/--source-url) or --context.",
                exit_code=ExitCode.USAGE_ERROR,
                code="invalid_input",
            ),
            command="fill new",
        )
    if no_download and output is not None:
        raise _as_typer_exit(
            ctx,
            CliError(
                "cannot use --output with --no-download.",
                exit_code=ExitCode.USAGE_ERROR,
                code="invalid_input",
            ),
            command="fill new",
        )

    _execute(
        ctx=ctx,
        command="fill new",
        runner=lambda client, reporter: run_fill_new(
            client,
            reporter,
            form_file=form_file,
            form_url=form_url,
            source_files=list(source_file),
            source_urls=list(source_url),
            context=context_text,
            instructions=instructions_text,
            wait=not no_wait,
            poll_interval_seconds=poll_interval_seconds,
            max_wait_seconds=max_wait_seconds,
            download=not no_download,
            output_path=output,
        ),
    )


@app.command(
    "existing",
    help=(
        "Autofill an existing form by form ID.\n\n"
        "Rules:\n"
        "- FORM_ID is required.\n"
        "- Use either --context or --context-file (not both).\n"
        "- Use either --instructions or --instructions-file (not both).\n"
        "- --output cannot be combined with --no-download.\n\n"
        "Examples:\n"
        "  simplicity-cli existing FORM_ID --context \"Use latest profile data\"\n"
        "  simplicity-cli existing FORM_ID --context-file ./context.txt --output ./filled.pdf\n"
        "  simplicity-cli existing FORM_ID --no-wait --no-download"
    ),
)
def existing_command(
    ctx: typer.Context,
    form_id: str = typer.Argument(..., help="Existing form ID."),
    context: str | None = typer.Option(None, "--context", help="Free-text context for autofill."),
    context_file: Path | None = typer.Option(
        None,
        "--context-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with autofill context.",
    ),
    instructions: str | None = typer.Option(None, "--instructions", help="AI instructions override."),
    instructions_file: Path | None = typer.Option(
        None,
        "--instructions-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with AI instructions.",
    ),
    no_wait: bool = typer.Option(False, "--no-wait", help="Do not wait for autofill completion."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
    no_download: bool = typer.Option(False, "--no-download", help="Do not download completed form."),
    output: Path | None = typer.Option(
        None,
        "--output",
        help="Output file path for downloaded PDF.",
    ),
) -> None:
    context_text = load_optional_text(context, context_file, "context")
    instructions_text = load_optional_text(instructions, instructions_file, "instructions")
    if no_download and output is not None:
        raise _as_typer_exit(
            ctx,
            CliError(
                "cannot use --output with --no-download.",
                exit_code=ExitCode.USAGE_ERROR,
                code="invalid_input",
            ),
            command="fill existing",
        )

    _execute(
        ctx=ctx,
        command="fill existing",
        runner=lambda client, reporter: run_fill_existing(
            client,
            reporter,
            form_id=form_id,
            context=context_text,
            instructions=instructions_text,
            wait=not no_wait,
            poll_interval_seconds=poll_interval_seconds,
            max_wait_seconds=max_wait_seconds,
            download=not no_download,
            output_path=output,
        ),
    )


@app.command(
    "status",
    help=(
        "Fetch current status/details for a task ID.\n\n"
        "Example:\n"
        "  simplicity-cli status TASK_ID"
    ),
)
def status_command(ctx: typer.Context, task_id: str = typer.Argument(..., help="Task ID.")) -> None:
    _execute(
        ctx=ctx,
        command="task status",
        runner=lambda client, _reporter: run_task_status(client, task_id=task_id),
    )


@app.command(
    "wait",
    help=(
        "Poll a task until it reaches a terminal state.\n\n"
        "Example:\n"
        "  simplicity-cli wait TASK_ID --poll-interval-seconds 2 --max-wait-seconds 1800"
    ),
)
def wait_command(
    ctx: typer.Context,
    task_id: str = typer.Argument(..., help="Task ID."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
) -> None:
    _execute(
        ctx=ctx,
        command="task wait",
        runner=lambda client, reporter: run_task_wait(
            client,
            reporter,
            task_id=task_id,
            poll_interval_seconds=poll_interval_seconds,
            max_wait_seconds=max_wait_seconds,
        ),
    )


def _execute(
    *,
    ctx: typer.Context,
    command: str,
    runner: Callable[[SimplicityApiClient, OutputReporter], dict[str, object]],
) -> None:
    options = _options_from_context(ctx)
    reporter = OutputReporter(options.json_output)
    try:
        config = resolve_runtime_config(options)
        with SimplicityApiClient(config) as client:
            payload = runner(client, reporter)
        reporter.emit_success(command, payload)
    except CliError as exc:
        raise _as_typer_exit(ctx, exc, command=command) from exc


def _as_typer_exit(ctx: typer.Context, error: CliError, command: str) -> typer.Exit:
    options = _options_from_context(ctx)
    reporter = OutputReporter(options.json_output)
    reporter.emit_error(command, error)
    return typer.Exit(code=int(error.exit_code))


def _options_from_context(ctx: typer.Context) -> GlobalOptions:
    options = ctx.obj
    if isinstance(options, GlobalOptions):
        return options
    return GlobalOptions(api_key=None, json_output=False, request_timeout_seconds=60.0)


@legacy_fill_app.command("new", help="Legacy alias for `new`.")
def legacy_fill_new_command(
    ctx: typer.Context,
    form_file: Path | None = typer.Option(
        None,
        "--form-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Local path to the PDF form.",
    ),
    form_url: str | None = typer.Option(None, "--form-url", help="URL to the form file."),
    source_file: list[Path] = typer.Option(
        [],
        "--source-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Source file path(s) used for autofill context.",
    ),
    source_url: list[str] = typer.Option(
        [],
        "--source-url",
        help="Source file URL(s) used for autofill context.",
    ),
    context: str | None = typer.Option(None, "--context", help="Free-text context for autofill."),
    context_file: Path | None = typer.Option(
        None,
        "--context-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with autofill context.",
    ),
    instructions: str | None = typer.Option(None, "--instructions", help="AI instructions override."),
    instructions_file: Path | None = typer.Option(
        None,
        "--instructions-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with AI instructions.",
    ),
    no_wait: bool = typer.Option(False, "--no-wait", help="Do not wait for autofill completion."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
    no_download: bool = typer.Option(False, "--no-download", help="Do not download completed form."),
    output: Path | None = typer.Option(
        None,
        "--output",
        help="Output file path for downloaded PDF.",
    ),
) -> None:
    new_command(
        ctx=ctx,
        form_file=form_file,
        form_url=form_url,
        source_file=source_file,
        source_url=source_url,
        context=context,
        context_file=context_file,
        instructions=instructions,
        instructions_file=instructions_file,
        no_wait=no_wait,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        no_download=no_download,
        output=output,
    )


@legacy_fill_app.command("existing", help="Legacy alias for `existing`.")
def legacy_fill_existing_command(
    ctx: typer.Context,
    form_id: str = typer.Argument(..., help="Existing form ID."),
    context: str | None = typer.Option(None, "--context", help="Free-text context for autofill."),
    context_file: Path | None = typer.Option(
        None,
        "--context-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with autofill context.",
    ),
    instructions: str | None = typer.Option(None, "--instructions", help="AI instructions override."),
    instructions_file: Path | None = typer.Option(
        None,
        "--instructions-file",
        file_okay=True,
        dir_okay=False,
        exists=True,
        readable=True,
        resolve_path=True,
        help="Path to a text file with AI instructions.",
    ),
    no_wait: bool = typer.Option(False, "--no-wait", help="Do not wait for autofill completion."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
    no_download: bool = typer.Option(False, "--no-download", help="Do not download completed form."),
    output: Path | None = typer.Option(
        None,
        "--output",
        help="Output file path for downloaded PDF.",
    ),
) -> None:
    existing_command(
        ctx=ctx,
        form_id=form_id,
        context=context,
        context_file=context_file,
        instructions=instructions,
        instructions_file=instructions_file,
        no_wait=no_wait,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
        no_download=no_download,
        output=output,
    )


@legacy_task_app.command("status", help="Legacy alias for `status`.")
def legacy_task_status_command(ctx: typer.Context, task_id: str = typer.Argument(..., help="Task ID.")) -> None:
    status_command(ctx=ctx, task_id=task_id)


@legacy_task_app.command("wait", help="Legacy alias for `wait`.")
def legacy_task_wait_command(
    ctx: typer.Context,
    task_id: str = typer.Argument(..., help="Task ID."),
    poll_interval_seconds: float = typer.Option(
        2.0,
        "--poll-interval-seconds",
        min=0.001,
        help="Polling interval while waiting for tasks.",
    ),
    max_wait_seconds: float = typer.Option(
        1800.0,
        "--max-wait-seconds",
        min=0.001,
        help="Maximum polling duration before timeout.",
    ),
) -> None:
    wait_command(
        ctx=ctx,
        task_id=task_id,
        poll_interval_seconds=poll_interval_seconds,
        max_wait_seconds=max_wait_seconds,
    )


def main() -> None:
    app()


if __name__ == "__main__":
    main()
