"""CLI utility functions for loading specs and building job configurations.

Shell functions for file I/O and CLI option handling.
Returns Result[T, E] where appropriate.
"""

import os

import json
import sys
from pathlib import Path

from pydantic import ValidationError
from rich.console import Console
from returns.result import Failure

from anima.core.persona_factory import build_inline_persona_fields
from anima.core.persona_import import parse_markdown_persona
from anima.core.execution import (
    ExecutionIntent,
    ExecutionIntentDefaults,
    ExecutionIntentOverrides,
    resolve_execution_contract,
)
from anima.core.schemas import (
    CompletionPolicy,
    ContextMode,
    JobSpec,
    LifecycleType,
    PersonaSpec,
)
from anima.shell.contract_compat import normalize_legacy_persona_payload
from anima.shell.model_config import load_global_config

console = Console()


# Default persona metadata for inline personas
INLINE_PERSONA_NAME = "inline-agent"
INLINE_PERSONA_DESCRIPTION = "Inline persona generated from CLI arguments."
INLINE_PERSONA_PROMPT = "You are a helpful assistant."


# @invar:allow shell_result: CLI resolver exits on invalid global config or missing model
# @invar:allow shell_complexity: Model resolution has 4 precedence levels (CLI/env/config/persona) with early exits
# @shell_complexity: Spec-source model-simplify.simplify-create-agent; precedence chain requires early-exit pattern
def _resolve_effective_model(cli_model: str | None, persona_model: str | None) -> str:
    """Resolve effective model from CLI/env/config/persona precedence.

    Spec source: user task precedence
    ``CLI > env > config.toml > PersonaSpec > error``.

    Args:
        cli_model: Model from CLI ``--model``.
        persona_model: Model from PersonaSpec (if any).

    Returns:
        Effective model string.

    Raises:
        SystemExit: If config is invalid or no model is available.
    """
    # Spec source: vectl step ``model-simplify.simplify-create-agent`` requires
    # loading config so ``[env]`` values can hydrate process env (non-overwrite).
    config_result = load_global_config()
    if isinstance(config_result, Failure):
        console.print(
            f"[red]Error loading global config: {config_result.failure()}[/red]"
        )
        sys.exit(1)
    global_config = config_result.unwrap()

    # Spec source: ``tests/test_cli.py::TestCliInlinePersona::"
    # ``test_run_inline_missing_model_shows_tutorial_error`` sets
    # ``ANIMA_DEFAULT_MODEL=""`` and expects tutorial guidance instead of a
    # contract failure in ``resolve_model_precedence``. Normalize blank env
    # values to ``None`` before calling the core resolver.
    env_model_raw = os.environ.get("ANIMA_DEFAULT_MODEL")
    env_model = env_model_raw if env_model_raw else None
    config_model: str | None = None
    if cli_model is None and env_model is None:
        config_model = global_config.defaults.model

    intent = ExecutionIntent(
        persona_spec=(
            PersonaSpec(
                id="model-resolution",
                description="Model resolution placeholder persona.",
                prompt="Model resolution placeholder prompt.",
                model=persona_model,
            )
            if persona_model is not None
            else None
        ),
        overrides=ExecutionIntentOverrides(
            requested_model=cli_model or env_model,
        ),
        defaults=ExecutionIntentDefaults(
            default_model=config_model,
        ),
    )
    try:
        effective_model = resolve_execution_contract(intent).runtime.effective_model
    except Exception:
        effective_model = ""

    if len(effective_model) == 0:
        # Spec source: user instruction for vectl step ``improve-model-error-msg``
        # requests an "error as tutorial" message with quick/permanent fixes,
        # config example reference, and precedence summary.
        console.print("[red]Error: No model configured for this run.[/red]")
        console.print(
            "\n"
            "[bold]Quick fix[/bold]\n"
            "  Re-run with an explicit model:\n"
            '  [cyan]anima run "<task>" --system-prompt "<prompt>" --model openai:gpt-5.2[/cyan]\n'
            "\n"
            "[bold]Permanent fix[/bold]\n"
            "  Set [cyan]defaults.model[/cyan] in [cyan]~/.anima/config.toml[/cyan]:\n"
            '  [cyan]model = "openai:gpt-5.2"[/cyan]\n'
            "\n"
            "[bold]Reference[/bold]\n"
            "  See [cyan]config.toml.example[/cyan] in the project root for full settings.\n"
            "\n"
            "[bold]Resolution order[/bold]\n"
            "  [cyan]--model[/cyan] → [cyan]ANIMA_DEFAULT_MODEL[/cyan] → "
            "[cyan]~/.anima/config.toml defaults.model[/cyan] → [cyan]PersonaSpec.model[/cyan]"
        )
        sys.exit(1)

    return effective_model


# @invar:allow shell_result: Persona file loader exits directly on invalid path or parse failure
def _load_and_validate_persona(path: Path) -> PersonaSpec:
    """Load and validate PersonaSpec from file path (JSON or markdown).

    Tries JSON parsing first. If JSONDecodeError, falls back to markdown
    parsing with YAML frontmatter support.

    Args:
        path: Path to persona spec file (.json or .md).

    Returns:
        Loaded PersonaSpec.

    Raises:
        SystemExit: If file invalid or not found.
    """
    if not path.exists():
        console.print(f"[red]Error: Persona file not found: {path}[/red]")
        sys.exit(1)

    try:
        content = path.read_text()
        # Try JSON first
        try:
            data = json.loads(content)
        except json.JSONDecodeError:
            # Fall back to markdown parsing
            data = parse_markdown_persona(content, fallback_name=path.stem)

        # Spec source: contract-alignment-integration.fix-seam-proof-blockers (B1)
        # Legacy aliases are quarantined in shell.contract_compat.
        data = normalize_legacy_persona_payload(data)

        # Filter to only PersonaSpec fields (ignore unknown fields)
        # This allows OpenCode-style frontmatter with non-spec fields
        valid_fields = PersonaSpec.model_fields.keys()
        filtered_data = {k: v for k, v in data.items() if k in valid_fields}

        return PersonaSpec(**filtered_data)
    except (OSError, ValidationError, TypeError, AttributeError) as e:
        # OSError: file I/O errors
        # ValidationError: PersonaSpec validation failures
        # TypeError: dict operations on non-dict (malformed frontmatter)
        # AttributeError: .get() on non-dict (non-mapping YAML)
        console.print(f"[red]Error loading persona: {e}[/red]")
        sys.exit(1)


# @invar:allow shell_result: persona wrapper exits for missing required CLI argument
def load_persona(persona: str | None) -> PersonaSpec:
    """Load PersonaSpec from file or exit with error.

    Args:
        persona: Path to persona spec file.

    Returns:
        Loaded PersonaSpec.

    Raises:
        SystemExit: If file not found or invalid.
    """
    if not persona:
        console.print("[red]Error: --persona is required[/red]")
        sys.exit(1)

    return _load_and_validate_persona(Path(persona))


# @invar:allow shell_result: load-or-synthesize entry path exits when both inputs are absent
def load_persona_or_synthesize(
    persona: str | None,
    system_prompt: str | None,
    model: str | None,
) -> PersonaSpec:
    """Load PersonaSpec from file or synthesize from inline parameters.

    Auto-detect logic: if value contains ``/`` or ``\\`` or has a file
    extension AND exists on disk, treat as file load; otherwise synthesize
    an inline persona.

    When neither persona nor system_prompt is provided, synthesizes an
    inline persona from --model with a default prompt.

    Args:
        persona: Optional persona name or path to PersonaSpec file.
        system_prompt: Optional inline system prompt text.
        model: Optional model identifier (from --model flag).

    Returns:
        Loaded or synthesized PersonaSpec.
    """
    if persona:
        path = Path(persona)
        # Auto-detect: file path if contains separator, has extension, or exists on disk
        _looks_like_file = (
            "/" in persona
            or "\\" in persona
            or "." in persona.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
        )
        if path.exists():
            loaded = _load_and_validate_persona(path)
            effective_model = _resolve_effective_model(
                cli_model=model,
                persona_model=loaded.model,
            )
            return loaded.model_copy(update={"model": effective_model})

        if _looks_like_file:
            # Looks like a file path but doesn't exist — hard fail
            console.print(f"[red]Error: Persona file not found: {persona}[/red]")
            sys.exit(1)

        # Non-path value: synthesize an inline persona
        effective_model = _resolve_effective_model(cli_model=model, persona_model=None)
        return PersonaSpec(
            **build_inline_persona_fields(
                model=effective_model,
                system_prompt=INLINE_PERSONA_PROMPT,
                persona_name=persona,
                description=f"Inline persona '{persona}'",
            )
        )

    # Otherwise synthesize from inline parameters.
    effective_prompt = system_prompt or INLINE_PERSONA_PROMPT
    effective_model = _resolve_effective_model(cli_model=model, persona_model=None)
    return PersonaSpec(
        **build_inline_persona_fields(
            model=effective_model,
            system_prompt=effective_prompt,
            persona_name=INLINE_PERSONA_NAME,
            description=INLINE_PERSONA_DESCRIPTION,
        )
    )


# Mapping of JobSpec field names to their enum types for CLI override conversion
_ENUM_FIELDS = {
    "context_mode": ContextMode,
    "completion_policy": CompletionPolicy,
}


# @invar:allow shell_result: JobSpec assembly exits when CLI overrides are invalid
# @shell_complexity: CLI option merging with enum conversion
def build_job_spec(
    job_file: str | None,
    persona_ref: str,
    instance_id: str,
    timeout: str | None,
    max_turns: int | None,
    max_tokens: int | None,
    context_mode: str | None,
    context_bundle_ref: str | None,
    completion_policy: str | None,
    lifecycle: LifecycleType,
) -> JobSpec:
    """Build JobSpec from CLI flags or file.

    CLI flags override JobSpec fields.

    Args:
        job_file: Optional path to JobSpec file.
        persona_ref: Reference to persona. **Standalone mode:** file path only.
            **Nervus integration:** Will support spec_id resolution via registry
            (not yet implemented). For now, use relative file paths.
        instance_id: Unique instance ID.
        timeout: Wall clock timeout.
        max_turns: Max agent turns.
        max_tokens: Token budget.
        context_mode: Context sharing mode.
        context_bundle_ref: Context bundle reference.
        completion_policy: Completion policy.
        lifecycle: Agent lifecycle type.

    Returns:
        JobSpec with CLI overrides applied.

    Raises:
        ValueError: If max_turns, max_tokens, or lifecycle have invalid values.
    """
    # Validate edge-case invalid values before JobSpec construction
    if max_turns is not None and max_turns < 1:
        raise ValueError(f"max_turns must be >= 1 if set, got {max_turns}")
    if max_tokens is not None and max_tokens < 1:
        raise ValueError(f"max_tokens must be >= 1 if set, got {max_tokens}")

    # Start with defaults or load from file
    if job_file:
        path = Path(job_file)
        try:
            content = path.read_text()
            data = json.loads(content)
            job = JobSpec(**data)
        except (OSError, json.JSONDecodeError, ValidationError) as e:
            console.print(f"[red]Error loading job spec: {e}[/red]")
            sys.exit(1)
    else:
        job = JobSpec(
            persona_ref=persona_ref,
            instance_id=instance_id,
        )

    # Apply CLI overrides using field mapping
    overrides = {
        "timeout": timeout,
        "max_turns": max_turns,
        "max_tokens": max_tokens,
        "context_mode": context_mode,
        "context_bundle_ref": context_bundle_ref,
        "completion_policy": completion_policy,
    }

    for field, value in overrides.items():
        if value is not None:
            if field in _ENUM_FIELDS:
                setattr(job, field, _ENUM_FIELDS[field](value))
            else:
                setattr(job, field, value)

    # Always set lifecycle (from CLI for spawn, ephemeral for run)
    job.lifecycle = lifecycle

    return job


def output_spawn_result(
    instance_id: str,
    lifecycle: LifecycleType,
    model: str,
    json_output: bool,
    pid: int | None = None,
    target_console: Console | None = None,
) -> None:
    """Output spawn result in JSON or Rich format.

    Args:
        instance_id: The instance ID.
        lifecycle: Agent lifecycle type.
        model: Model name.
        json_output: Whether to output JSON.
        pid: Optional process ID.
        target_console: Console to write to. Defaults to module-level console.
    """
    out = target_console if target_console is not None else console
    if json_output:
        output = {
            "instance_id": instance_id,
            "state": "pending",
            "pid": pid,
        }
        out.print_json(data=output)
    else:
        from rich.panel import Panel

        panel = Panel(
            f"[bold]Instance ID:[/] {instance_id}\n"
            f"[bold]Lifecycle:[/] {lifecycle.value}\n"
            f"[bold]Model:[/] {model}",
            title="anima spawn",
            border_style="green",
        )
        out.print(panel)
