"""Job commands for manta-client CLI."""

from __future__ import annotations

import asyncio
from pathlib import Path
from typing import Any

import typer

from manta_client.api.agent.job import get_job_api
from manta_client.api.client import APIError
from manta_client.exitcodes import (
    JOB_CANCELLED,
    JOB_FAILED,
    JOB_TIMEOUT,
    VALIDATION_ERROR,
    exit_code_for_api_error,
)
from manta_client.output import (
    console,
    is_quiet,
    print_error,
    print_json,
    print_key_value,
    print_success,
    print_table,
)

app = typer.Typer(help="Job management")

# Terminal job states
_TERMINAL_STATES = {"completed", "failed", "cancelled"}


async def _cache_job(job: dict[str, Any]) -> None:
    """Best-effort upsert job to local DB."""
    try:
        from manta_client.db import upsert_job

        await upsert_job(job)
    except Exception:
        pass


async def _cache_jobs(jobs: list[dict[str, Any]]) -> None:
    """Best-effort batch upsert jobs to local DB."""
    try:
        from manta_client.db import upsert_jobs

        await upsert_jobs(jobs)
    except Exception:
        pass


async def _try_sync_session(job: dict[str, Any]) -> None:
    """Best-effort download session file for a completed job."""
    session_id = job.get("session_id")
    if not session_id or job.get("status") != "completed":
        return

    from manta_client.db import get_local_job, mark_session_synced, session_file_path

    dest = session_file_path(session_id)
    if dest.exists():
        return

    # Check local DB flag to avoid redundant download attempts
    try:
        local = await get_local_job(job.get("job_id", ""))
        if local and local.get("session_synced"):
            return
    except Exception:
        pass

    try:
        api = get_job_api()
        content = await api.download_session(job["job_id"])
        dest.parent.mkdir(parents=True, exist_ok=True)
        dest.write_bytes(content)
        await mark_session_synced(job["job_id"])
    except Exception:
        pass  # best-effort, never block the command


def _format_status(status: str) -> str:
    """Format job status with color."""
    status_colors = {
        "pending": "[yellow]pending[/yellow]",
        "running": "[blue]running[/blue]",
        "completed": "[green]completed[/green]",
        "failed": "[red]failed[/red]",
        "cancelled": "[dim]cancelled[/dim]",
    }
    return status_colors.get(status, status)


def _exit_for_api_error(e: APIError) -> None:
    """Print error and exit with appropriate code."""
    print_error(e.message)
    raise typer.Exit(exit_code_for_api_error(e.code))


@app.command("create")
def create_job(
    prompt_text: str = typer.Option(
        "",
        "--prompt",
        "-p",
        help="Prompt message for the job",
    ),
    message: str = typer.Option(
        "",
        "--message",
        "-m",
        help="[deprecated] Use --prompt/-p instead",
        hidden=True,
    ),
    input_json: str = typer.Option(
        None,
        "--input",
        "-i",
        help="Input data as JSON string",
    ),
    skill: list[str] = typer.Option(  # noqa: B008
        None,
        "--skill",
        "-s",
        help="Skill slug for prompt injection (repeatable)",
    ),
    workspace: str = typer.Option(
        None,
        "--workspace",
        "-w",
        help="Workspace name (for repos and cluster)",
    ),
    conversation: str = typer.Option(
        None,
        "--conversation",
        "-C",
        help="Continue from a previous job ID",
    ),
    model: str = typer.Option(
        None,
        "--model",
        "-M",
        help="LLM model (e.g., claude-sonnet-4-5, opus, haiku)",
    ),
    config_file: str = typer.Option(
        None,
        "--file",
        "-F",
        help="Load job parameters from YAML/JSON config file",
    ),
    follow: bool = typer.Option(
        False,
        "--follow",
        "-f",
        help="Follow job execution logs",
    ),
    timeout: int = typer.Option(
        0,
        "--timeout",
        "-T",
        help="Timeout in seconds for --follow mode (0 = no limit)",
    ),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """Create a new async job.

    The job will be executed asynchronously in a sandbox.
    Use --follow to stream job logs in real-time.
    Use -F to load parameters from a YAML config file (CLI flags override file values).
    """
    import json as json_lib

    # Backward compat: --message/-m → --prompt/-p
    if message and not prompt_text:
        from rich.console import Console

        err_console = Console(stderr=True)
        err_console.print(
            "[yellow]Warning: --message/-m is deprecated, use --prompt/-p instead[/yellow]"
        )
        prompt_text = message

    # Load config file if provided (CLI flags override file values)
    if config_file:
        file_config = _load_config_file(config_file)
        if not prompt_text and (file_config.get("prompt") or file_config.get("message")):
            prompt_text = file_config.get("prompt") or file_config["message"]
        if not skill and (file_config.get("skills") or file_config.get("skill_slugs")):
            skill = list(file_config.get("skills") or file_config["skill_slugs"])
        if not conversation and file_config.get("conversation"):
            conversation = file_config["conversation"]
        if not input_json and file_config.get("input"):
            input_json = json_lib.dumps(file_config["input"])
        if not model and file_config.get("model"):
            model = file_config["model"]

    # Parse input JSON if provided
    input_data = None
    if input_json:
        try:
            input_data = json_lib.loads(input_json)
        except json_lib.JSONDecodeError as e:
            print_error(f"Invalid JSON input: {e}")
            raise typer.Exit(VALIDATION_ERROR) from None

    async def _create():
        # Resolve workspace name to ID
        workspace_id = None
        if workspace:
            from manta_client.api.agent.workspace import get_workspace_api

            ws_api = get_workspace_api()
            ws = await ws_api.get_workspace_by_name(workspace)
            workspace_id = ws.get("id")
            if not workspace_id:
                raise ValueError(f"Workspace '{workspace}' not found")

        api = get_job_api()
        return await api.create_job(
            prompt=prompt_text,
            input_data=input_data,
            workspace_id=workspace_id,
            skill_slugs=skill if skill else None,
            conversation=conversation,
            model=model,
        )

    try:
        job = asyncio.run(_create())
    except APIError as e:
        _exit_for_api_error(e)
    except ValueError as e:
        print_error(str(e))
        raise typer.Exit(VALIDATION_ERROR) from None

    asyncio.run(_cache_job(job))

    job_id = job.get("job_id", "")

    # Quiet mode: only output job_id
    if is_quiet():
        print(job_id)
        if follow:
            _follow_job_logs(job_id, timeout=timeout or None)
        return

    if json_output and not follow:
        print_json(job)
        return

    print_success(f"Job created: {job_id}")
    console.print(f"Status: {_format_status(job.get('status', 'pending'))}")

    if follow:
        console.print("\n[cyan]Following job logs...[/cyan]")
        _follow_job_logs(job_id, timeout=timeout or None)


@app.command("list")
def list_jobs(
    limit: int = typer.Option(20, "--limit", "-l", help="Maximum number of jobs to show"),
    status: str | None = typer.Option(
        None,
        "--status",
        "-s",
        help="Filter by status (pending/running/completed/failed/cancelled)",
    ),
    all_users: bool = typer.Option(False, "--all", "-a", help="Show all users' jobs"),
    user: str | None = typer.Option(None, "--user", "-u", help="Filter by user ID"),
    search: str | None = typer.Option(None, "--search", "-S", help="Search in title/prompt"),
    since: str | None = typer.Option(
        None,
        "--since",
        help="Jobs created after (ISO8601 or YYYY-MM-DD)",
    ),
    before: str | None = typer.Option(
        None,
        "--before",
        help="Jobs created before (ISO8601 or YYYY-MM-DD)",
    ),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """List recent jobs. By default shows only your own jobs."""
    from manta_client.config import config_manager

    VALID_STATUSES = {"pending", "running", "completed", "failed", "cancelled"}

    if all_users and user:
        print_error("--all and --user are mutually exclusive")
        raise typer.Exit(VALIDATION_ERROR)

    if status and status not in VALID_STATUSES:
        print_error(f"Invalid status '{status}'. Valid: {', '.join(sorted(VALID_STATUSES))}")
        raise typer.Exit(VALIDATION_ERROR)

    async def _list():
        api = get_job_api()
        # Determine user_id filter
        filter_user_id = None
        if user:
            filter_user_id = user
        elif not all_users:
            config = config_manager.load_config()
            filter_user_id = config.user_id or None
            if not filter_user_id:
                console.print("[dim]Not logged in, showing all jobs. Use --user to filter.[/dim]")
        return await api.list_jobs(
            limit=limit,
            status=status,
            user_id=filter_user_id,
            search=search,
            since=since,
            before=before,
        )

    try:
        result = asyncio.run(_list())
    except APIError as e:
        _exit_for_api_error(e)

    jobs = result.get("items", [])
    total = result.get("total", 0)

    asyncio.run(_cache_jobs(jobs))

    if json_output:
        print_json(result)
        return

    if not jobs:
        if not is_quiet():
            console.print("[dim]No jobs found.[/dim]")
        return

    # Quiet mode: one job_id per line
    if is_quiet():
        for job in jobs:
            print(job.get("job_id", ""))
        return

    # Format for table display
    for job in jobs:
        job["status"] = _format_status(job.get("status", ""))
        # Use title if available, otherwise truncate prompt
        title = job.get("title") or ""
        if not title:
            prompt = job.get("prompt", "")
            title = (prompt[:47] + "...") if len(prompt) > 50 else prompt
        elif len(title) > 50:
            title = title[:47] + "..."
        job["_display_title"] = title

    columns = [
        ("job_id", "Job ID"),
        ("status", "Status"),
        ("_display_title", "Title"),
        ("created_at", "Created"),
    ]
    # Show user column when viewing multiple users' jobs
    if all_users or user:
        columns.insert(2, ("user_id", "User"))

    print_table(jobs, columns=columns, title=f"Jobs ({total})", id_field="job_id")


@app.command("info")
def job_info(
    job_id: str = typer.Argument(..., help="Job ID"),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """Show job details."""

    async def _get_info():
        api = get_job_api()
        return await api.get_job(job_id)

    try:
        job = asyncio.run(_get_info())
    except APIError as e:
        _exit_for_api_error(e)

    asyncio.run(_cache_job(job))
    asyncio.run(_try_sync_session(job))

    if json_output:
        print_json(job)
        return

    # Quiet mode: print status only
    if is_quiet():
        print(job.get("status", ""))
        return

    # Format for display
    data: dict[str, Any] = {
        "Job ID": job.get("job_id"),
        "Title": job.get("title") or "-",
        "Status": job.get("status"),
        "Prompt": job.get("prompt") or "-",
        "Parent Job": job.get("parent_job_id") or "-",
        "Created At": job.get("created_at") or "-",
        "Updated At": job.get("updated_at") or "-",
    }

    print_key_value(data, title="Job Details")

    # Show summary if available
    summary = job.get("summary")
    if summary:
        console.print(f"\n[bold]Summary:[/bold] {summary}")

    # Show result or error
    result = job.get("result")
    error = job.get("error")

    if result:
        console.print("\n[bold]Result:[/bold]")
        console.print(result)

    if error:
        console.print(f"\n[red]Error: {error}[/red]")

    # Show artifacts if available
    artifacts = job.get("artifacts")
    if artifacts:
        console.print("\n[bold]Artifacts:[/bold]")
        print_json(artifacts)


@app.command("wait")
def wait_job(
    job_id: str = typer.Argument(..., help="Job ID"),
    timeout: int = typer.Option(
        0,
        "--timeout",
        "-T",
        help="Timeout in seconds (0 = wait forever)",
    ),
    poll_interval: int = typer.Option(
        3,
        "--interval",
        help="Poll interval in seconds",
    ),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """Wait for a job to reach a terminal state.

    Polls the job status until it completes, fails, or is cancelled.
    Useful for scripts and AI agents that need to wait for results.

    Exit codes:
      0  = completed
      10 = failed
      11 = cancelled
      12 = timeout
    """
    import time

    start = time.monotonic()

    async def _poll():
        api = get_job_api()
        while True:
            job = await api.get_job(job_id)
            status = job.get("status", "")

            if status in _TERMINAL_STATES:
                return job

            # Check timeout
            if timeout > 0 and (time.monotonic() - start) >= timeout:
                return None

            if not is_quiet():
                elapsed = int(time.monotonic() - start)
                console.print(f"[dim]Waiting... status={status} elapsed={elapsed}s[/dim]")

            await asyncio.sleep(poll_interval)

    try:
        job = asyncio.run(_poll())
    except APIError as e:
        _exit_for_api_error(e)

    if job is not None:
        asyncio.run(_cache_job(job))
        asyncio.run(_try_sync_session(job))

    # Timeout
    if job is None:
        if is_quiet():
            print("timeout")
        else:
            print_error(f"Timeout after {timeout}s waiting for job {job_id}")
        raise typer.Exit(JOB_TIMEOUT)

    status = job.get("status", "")

    if json_output:
        print_json(job)
    elif is_quiet():
        print(status)
    else:
        console.print(f"Job {job_id}: {_format_status(status)}")
        summary = job.get("summary")
        if summary:
            console.print(f"Summary: {summary}")
        result = job.get("result")
        if result:
            console.print("\n[bold]Result:[/bold]")
            console.print(result)
        error = job.get("error")
        if error:
            console.print(f"\n[red]Error: {error}[/red]")

    # Exit code based on terminal state
    if status == "completed":
        raise typer.Exit(0)
    elif status == "failed":
        raise typer.Exit(JOB_FAILED)
    elif status == "cancelled":
        raise typer.Exit(JOB_CANCELLED)


def _follow_job_logs(job_id: str, *, timeout: int | None = None) -> None:
    """Follow job logs via SSE with optional timeout."""
    import time

    async def _stream():
        api = get_job_api()
        final_status = None
        start = time.monotonic()

        try:
            async for event in api.stream_job(job_id):
                # Check timeout
                if timeout and (time.monotonic() - start) >= timeout:
                    console.print(f"\n[yellow]Timeout after {timeout}s.[/yellow]")
                    raise typer.Exit(JOB_TIMEOUT)

                # Handle both "type" and "event_type" field names (API uses event_type)
                event_type = event.get("type") or event.get("event_type", "")
                status = event.get("status")
                data = event.get("data", {})

                if event_type == "job_state":
                    if status:
                        if not is_quiet():
                            console.print(f"[dim]Status: {status}[/dim]")
                        final_status = status

                    # Check for completion
                    if status in _TERMINAL_STATES:
                        error = event.get("error")
                        if error:
                            console.print(f"\n[red]Error: {error}[/red]")

                        # SSE does not carry result — fetch full job info
                        if status == "completed":
                            try:
                                job = await api.get_job(job_id)
                                await _cache_job(job)
                                await _try_sync_session(job)
                                result = job.get("result")
                                if result and not is_quiet():
                                    console.print("\n[bold]Result:[/bold]")
                                    console.print(result)
                            except Exception:
                                if not is_quiet():
                                    console.print(
                                        "\n[dim]Job completed. Use 'manta-client job info'"
                                        " to view result.[/dim]"
                                    )

                        break

                elif event_type in ("log", "agent_output"):
                    # Stream log/agent output
                    # Support both "message" and "data.output" formats
                    output = data.get("output") if isinstance(data, dict) else ""
                    message = event.get("message") or output
                    if message:
                        console.print(message, end="")

                else:
                    # Other event types (sandbox_starting, etc.)
                    if data and not is_quiet():
                        console.print(f"[dim]{event_type}: {data}[/dim]")

        except KeyboardInterrupt:
            console.print("\n[yellow]Stream interrupted.[/yellow]")
        except typer.Exit:
            raise
        except Exception as e:
            console.print(f"\n[red]Stream error: {e}[/red]")

        return final_status

    asyncio.run(_stream())


@app.command("logs")
def job_logs(
    job_id: str = typer.Argument(..., help="Job ID"),
    follow: bool = typer.Option(
        False,
        "--follow",
        "-f",
        help="Follow logs in real-time",
    ),
    timeout: int = typer.Option(
        0,
        "--timeout",
        "-T",
        help="Timeout in seconds for --follow mode (0 = no limit)",
    ),
) -> None:
    """Show job execution logs.

    Use --follow to stream logs in real-time.
    Use --timeout to limit follow duration (prevents hanging).
    """
    if follow:
        if not is_quiet():
            console.print(f"[cyan]Following logs for job {job_id}...[/cyan]")
        _follow_job_logs(job_id, timeout=timeout or None)
    else:
        # Get current job status and show available info
        async def _get_job():
            api = get_job_api()
            return await api.get_job(job_id)

        try:
            job = asyncio.run(_get_job())
        except APIError as e:
            _exit_for_api_error(e)

        asyncio.run(_cache_job(job))
        asyncio.run(_try_sync_session(job))

        status = job.get("status", "")

        if is_quiet():
            print(status)
            return

        console.print(f"Job ID: {job_id}")
        console.print(f"Status: {_format_status(status)}")

        result = job.get("result")
        error = job.get("error")

        if result:
            console.print("\n[bold]Output:[/bold]")
            console.print(result)

        if error:
            console.print(f"\n[red]Error: {error}[/red]")

        if status in ("pending", "running"):
            console.print("\n[dim]Job is still running. Use --follow to stream logs.[/dim]")


@app.command("cancel")
def cancel_job(
    job_id: str = typer.Argument(..., help="Job ID"),
) -> None:
    """Cancel a running job."""

    async def _cancel():
        api = get_job_api()
        await api.cancel_job(job_id)
        # Fetch updated job info for local cache
        return await api.get_job(job_id)

    try:
        job = asyncio.run(_cancel())
        asyncio.run(_cache_job(job))
        if is_quiet():
            print("cancelled")
        else:
            print_success(f"Job {job_id} cancelled.")
    except APIError as e:
        _exit_for_api_error(e)


@app.command("retry")
def retry_job(
    job_id: str = typer.Argument(..., help="Job ID to retry"),
    follow: bool = typer.Option(
        False,
        "--follow",
        "-f",
        help="Follow the new job's execution logs",
    ),
    timeout: int = typer.Option(
        0,
        "--timeout",
        "-T",
        help="Timeout in seconds for --follow mode (0 = no limit)",
    ),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """Retry a failed or cancelled job.

    Creates a new job with the same parameters as the original.
    """

    async def _retry():
        api = get_job_api()
        return await api.retry_job(job_id)

    try:
        new_job = asyncio.run(_retry())
    except APIError as e:
        _exit_for_api_error(e)

    asyncio.run(_cache_job(new_job))

    new_job_id = new_job.get("job_id", "")

    if is_quiet():
        print(new_job_id)
        if follow:
            _follow_job_logs(new_job_id, timeout=timeout or None)
        return

    if json_output and not follow:
        print_json(new_job)
        return

    print_success(f"Retried job {job_id} → new job: {new_job_id}")
    console.print(f"Status: {_format_status(new_job.get('status', 'pending'))}")

    if follow:
        console.print("\n[cyan]Following job logs...[/cyan]")
        _follow_job_logs(new_job_id, timeout=timeout or None)


@app.command("artifacts")
def list_artifacts(
    job_id: str = typer.Argument(..., help="Job ID"),
    json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
) -> None:
    """List artifacts for a job."""

    async def _list():
        api = get_job_api()
        return await api.list_artifacts(job_id)

    try:
        result = asyncio.run(_list())
    except APIError as e:
        _exit_for_api_error(e)

    artifacts = result.get("artifacts", [])
    total = result.get("total", 0)

    if json_output:
        print_json(result)
        return

    if not artifacts:
        if not is_quiet():
            console.print("[dim]No artifacts found.[/dim]")
        return

    if is_quiet():
        for a in artifacts:
            print(a.get("name", ""))
        return

    columns = [
        ("name", "Filename"),
        ("size", "Size (bytes)"),
        ("last_modified", "Last Modified"),
    ]

    print_table(artifacts, columns=columns, title=f"Artifacts for {job_id} ({total})")


@app.command("download")
def download_artifact(
    job_id: str = typer.Argument(..., help="Job ID"),
    filename: str = typer.Argument(..., help="Artifact filename"),
    output: str = typer.Option(
        None,
        "--output",
        "-o",
        help="Output file path (default: current directory)",
    ),
) -> None:
    """Download an artifact file."""

    async def _download():
        api = get_job_api()
        return await api.download_artifact(job_id, filename)

    try:
        content = asyncio.run(_download())
    except APIError as e:
        _exit_for_api_error(e)

    # Determine output path
    if output:
        output_path = Path(output)
    else:
        output_path = Path.cwd() / filename

    # Create parent directories if needed
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # Write file
    output_path.write_bytes(content)
    if is_quiet():
        print(str(output_path))
    else:
        print_success(f"Downloaded to: {output_path}")
        console.print(f"Size: {len(content)} bytes")


def _load_config_file(file_path: str) -> dict[str, Any]:
    """Load job parameters from a YAML or JSON config file."""
    import json as json_lib

    import yaml

    path = Path(file_path)
    if not path.exists():
        print_error(f"Config file not found: {file_path}")
        raise typer.Exit(VALIDATION_ERROR)

    content = path.read_text()
    try:
        if path.suffix in (".yaml", ".yml"):
            return yaml.safe_load(content) or {}
        elif path.suffix == ".json":
            return json_lib.loads(content)
        else:
            # Try YAML first, fall back to JSON
            try:
                return yaml.safe_load(content) or {}
            except yaml.YAMLError:
                return json_lib.loads(content)
    except Exception as e:
        print_error(f"Failed to parse config file: {e}")
        raise typer.Exit(VALIDATION_ERROR) from None
