"""CLI shell adapter for facade-backed persona commands."""

from __future__ import annotations

import argparse
import json
import sys
from typing import IO, Callable, Sequence, cast

from returns.result import Failure, Result, Success

from larva.app.facade import (
    AssembleRequest,
    ClearedRegistry,
    DeletedPersona,
    LarvaFacade,
)
from larva.core.spec import PersonaSpec
from larva.core.validate import ValidationReport
from larva.shell.components import ComponentStore, FilesystemComponentStore
from larva.shell.registry import CLEAR_CONFIRMATION_TOKEN
from larva.shell.cli_helpers import (
    EXIT_CRITICAL,
    EXIT_ERROR,
    EXIT_OK,
    CliCommandResult,
    CliExitCode,
    CliFailure,
    JsonErrorEnvelope,  # noqa: F401  # re-exported from this module for callers/tests
    _CliParseError,
    _build_parser,
    _component_show_invalid_target,
    _critical_error,
    _emit_result,
    _map_component_error,
    _map_facade_error,
    _operation_failure,
    _parse_key_value_pairs,
    _parse_set_values,
    _read_spec_json,
    _render_payload_for_text,
    _render_validation_report,
    _write_output_json,
    build_default_facade,
)

PERSONA_COMMAND_SEAM = "larva.app.facade.LarvaFacade"
COMPONENT_COMMAND_SEAM = "larva.shell.components.ComponentStore"


def _dispatch_component(
    args: argparse.Namespace,
    *,
    as_json: bool,
    component_store: ComponentStore,
) -> Result[CliCommandResult, CliFailure]:
    component_command = cast("str", getattr(args, "component_command", ""))
    if component_command == "list":
        return component_list_command(as_json=as_json, component_store=component_store)
    if component_command == "show":
        return component_show_command(
            cast("str", args.ref),
            as_json=as_json,
            component_store=component_store,
        )
    return Failure(
        {
            "exit_code": EXIT_CRITICAL,
            "stderr": f"Unsupported component command: {component_command}\n",
            "error": _critical_error(
                "unsupported component command", {"command": component_command}
            ),
        }
    )


def _dispatch_validate(
    args: argparse.Namespace,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    loaded_spec = _read_spec_json(cast("str", args.spec))
    if isinstance(loaded_spec, Failure):
        return Failure(_operation_failure("Validate", loaded_spec.failure(), as_json=as_json))
    return validate_command(loaded_spec.unwrap(), as_json=as_json, facade=facade)


def _build_assemble_request(
    args: argparse.Namespace,
) -> Result[AssembleRequest, JsonErrorEnvelope]:
    overrides = _parse_key_value_pairs(cast("list[str]", args.overrides), flag="--override")
    if isinstance(overrides, Failure):
        return Failure(overrides.failure())
    variables = _parse_key_value_pairs(cast("list[str]", args.variables), flag="--var")
    if isinstance(variables, Failure):
        return Failure(variables.failure())

    request: AssembleRequest = {
        "id": cast("str", args.id),
        "prompts": cast("list[str]", args.prompts),
        "toolsets": cast("list[str]", args.toolsets),
        "constraints": cast("list[str]", args.constraints),
        "overrides": overrides.unwrap(),
        "variables": cast("dict[str, str]", variables.unwrap()),
    }
    model = cast("str | None", args.model)
    if model is not None:
        request["model"] = model
    return Success(request)


def _dispatch_assemble(
    args: argparse.Namespace,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    request_result = _build_assemble_request(args)
    if isinstance(request_result, Failure):
        return Failure(_operation_failure("Assemble", request_result.failure(), as_json=as_json))
    return assemble_command(
        request_result.unwrap(),
        as_json=as_json,
        facade=facade,
        output_path=cast("str | None", args.output),
    )


def _dispatch_register(
    args: argparse.Namespace,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    loaded_spec = _read_spec_json(cast("str", args.spec))
    if isinstance(loaded_spec, Failure):
        return Failure(_operation_failure("Register", loaded_spec.failure(), as_json=as_json))
    return register_command(loaded_spec.unwrap(), as_json=as_json, facade=facade)


def _dispatch_resolve(
    args: argparse.Namespace,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    overrides = _parse_key_value_pairs(cast("list[str]", args.overrides), flag="--override")
    if isinstance(overrides, Failure):
        return Failure(_operation_failure("Resolve", overrides.failure(), as_json=as_json))
    return resolve_command(
        cast("str", args.id),
        overrides=overrides.unwrap(),
        as_json=as_json,
        facade=facade,
    )


def _dispatch_update(
    args: argparse.Namespace,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    patches_result = _parse_set_values(cast("list[str]", args.set_values), flag="--set")
    if isinstance(patches_result, Failure):
        return Failure(_operation_failure("Update", patches_result.failure(), as_json=as_json))
    return update_command(
        cast("str", args.id),
        patches=patches_result.unwrap(),
        as_json=as_json,
        facade=facade,
    )


def _dispatch(
    args: argparse.Namespace,
    *,
    facade: LarvaFacade,
    component_store: ComponentStore,
) -> Result[CliCommandResult, CliFailure]:
    command = cast("str", args.command)
    as_json = cast("bool", getattr(args, "as_json", False))

    command_dispatchers: dict[str, Callable[[], Result[CliCommandResult, CliFailure]]] = {
        "validate": lambda: _dispatch_validate(args, as_json=as_json, facade=facade),
        "assemble": lambda: _dispatch_assemble(args, as_json=as_json, facade=facade),
        "register": lambda: _dispatch_register(args, as_json=as_json, facade=facade),
        "resolve": lambda: _dispatch_resolve(args, as_json=as_json, facade=facade),
        "clone": lambda: clone_command(
            cast("str", args.source_id), cast("str", args.new_id), as_json=as_json, facade=facade
        ),
        "list": lambda: list_command(as_json=as_json, facade=facade),
        "export": lambda: export_command(
            ids=cast("list[str]", args.ids),
            export_all=cast("bool", args.export_all),
            as_json=as_json,
            facade=facade,
        ),
        "delete": lambda: delete_command(cast("str", args.id), as_json=as_json, facade=facade),
        "clear": lambda: clear_command(
            confirm=cast("str", args.confirm), as_json=as_json, facade=facade
        ),
        "update": lambda: _dispatch_update(args, as_json=as_json, facade=facade),
        "component": lambda: _dispatch_component(
            args, as_json=as_json, component_store=component_store
        ),
    }
    dispatch = command_dispatchers.get(command)
    if dispatch is not None:
        return dispatch()

    return Failure(
        {
            "exit_code": EXIT_CRITICAL,
            "stderr": f"Unsupported command: {command}\n",
            "error": _critical_error("unsupported command", {"command": command}),
        }
    )


# @invar:allow shell_result: CLI entry handler returns process exit code
def run_cli(
    argv: Sequence[str],
    *,
    facade: LarvaFacade,
    stdout: IO[str],
    stderr: IO[str],
    component_store: ComponentStore | None = None,
) -> CliExitCode:
    parser = _build_parser()
    active_component_store = (
        component_store if component_store is not None else FilesystemComponentStore()
    )
    try:
        args = parser.parse_args(list(argv))
    except _CliParseError as error:
        parse_failure: CliFailure = {
            "exit_code": EXIT_CRITICAL,
            "stderr": f"Argument parsing failed: {error}\n",
            "error": _critical_error("argument parsing failed", {"message": str(error)}),
        }
        return _emit_result(
            Failure(parse_failure),
            as_json="--json" in argv,
            stdout=stdout,
            stderr=stderr,
        )

    return _emit_result(
        _dispatch(args, facade=facade, component_store=active_component_store),
        as_json=cast("bool", getattr(args, "as_json", False)),
        stdout=stdout,
        stderr=stderr,
    )


# @invar:allow shell_result: process entrypoint returns int exit code
# @invar:allow dead_export: console script entrypoint referenced via pyproject script
def main(argv: Sequence[str] | None = None) -> int:
    active_argv = list(sys.argv[1:] if argv is None else argv)
    return int(
        run_cli(
            active_argv,
            facade=build_default_facade(),
            stdout=sys.stdout,
            stderr=sys.stderr,
            component_store=FilesystemComponentStore(),
        )
    )


def validate_command(
    spec: PersonaSpec,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    report = facade.validate(spec)
    if report["valid"]:
        return _validation_success_result(report, as_json=as_json)
    return _validation_failure_result(report, as_json=as_json)


def _validation_success_result(
    report: ValidationReport, *, as_json: bool
) -> Result[CliCommandResult, CliFailure]:
    result: CliCommandResult = {
        "exit_code": EXIT_OK,
        "stdout": _render_validation_report(report),
    }
    if as_json:
        result["json"] = {
            "data": {
                "valid": True,
                "errors": [],
                "warnings": cast("list[str]", report.get("warnings", [])),
            }
        }
    return Success(result)


def _validation_failure_result(
    report: ValidationReport, *, as_json: bool
) -> Result[CliCommandResult, CliFailure]:
    errors = cast("list[dict[str, object]]", report.get("errors", []))
    message = "validation failed"
    if errors:
        message = cast("str", errors[0].get("message", message))
    error_envelope = _map_facade_error(
        {
            "code": "PERSONA_INVALID",
            "numeric_code": 101,
            "message": message,
            "details": {"report": report},
        }
    )
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Validation failed: {error_envelope['message']}\n"
    return Failure(failure)


def assemble_command(
    request: AssembleRequest,
    *,
    as_json: bool,
    facade: LarvaFacade,
    output_path: str | None = None,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.assemble(request)
    if isinstance(result, Success):
        payload = dict(result.unwrap())
        return _assemble_success_result(
            payload,
            as_json=as_json,
            output_path=output_path,
        )

    error_envelope = _map_facade_error(result.failure())
    return _assemble_failure_result(error_envelope, exit_code=EXIT_ERROR, as_json=as_json)


def _assemble_success_result(
    payload: dict[str, object],
    *,
    as_json: bool,
    output_path: str | None,
) -> Result[CliCommandResult, CliFailure]:
    if output_path is not None:
        write_result = _write_output_json(output_path, payload)
        if isinstance(write_result, Failure):
            return _assemble_failure_result(
                write_result.failure(),
                exit_code=EXIT_CRITICAL,
                as_json=as_json,
            )

    cli_result: CliCommandResult = {
        "exit_code": EXIT_OK,
        "stdout": "" if output_path is not None else _render_payload_for_text("assemble", payload),
    }
    if as_json:
        cli_result["json"] = {"data": payload}
    return Success(cli_result)


def _assemble_failure_result(
    error_envelope: JsonErrorEnvelope,
    *,
    exit_code: CliExitCode,
    as_json: bool,
) -> Result[CliCommandResult, CliFailure]:
    failure: CliFailure = {"exit_code": exit_code, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Assembly failed: {error_envelope['message']}\n"
    return Failure(failure)


def register_command(
    spec: PersonaSpec,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.register(spec)
    if isinstance(result, Success):
        payload = dict(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("register", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Registration failed: {error_envelope['message']}\n"
    return Failure(failure)


def resolve_command(
    persona_id: str,
    *,
    overrides: dict[str, object] | None = None,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.resolve(persona_id, overrides=overrides)
    if isinstance(result, Success):
        payload = dict(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("resolve", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Resolve failed: {error_envelope['message']}\n"
    return Failure(failure)


def list_command(*, as_json: bool, facade: LarvaFacade) -> Result[CliCommandResult, CliFailure]:
    result = facade.list()
    if isinstance(result, Success):
        payload = list(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("list", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"List failed: {error_envelope['message']}\n"
    return Failure(failure)


def clone_command(
    source_id: str,
    new_id: str,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.clone(source_id, new_id)
    if isinstance(result, Success):
        payload = dict(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("clone", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Clone failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: text output renders specs as pretty JSON separated by ---
def export_command(
    ids: list[str],
    *,
    export_all: bool,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    # Validate mutual exclusion: --all xor ids
    if export_all and ids:
        error_envelope: JsonErrorEnvelope = {
            "code": "ARGUMENT_CONFLICT",
            "numeric_code": 113,
            "message": "Cannot specify both --all and persona ids",
            "details": {},
        }
        failure: CliFailure = {"exit_code": EXIT_CRITICAL, "error": error_envelope}
        if not as_json:
            failure["stderr"] = f"Export failed: {error_envelope['message']}\n"
        return Failure(failure)

    result = facade.export_all() if export_all else facade.export_ids(ids)
    if isinstance(result, Success):
        specs = list(result.unwrap())

        # Text mode: pretty JSON for each spec, separated by ---
        text_output = ""
        if specs:
            text_parts = [
                json.dumps(spec, indent=2, sort_keys=True, ensure_ascii=True) for spec in specs
            ]
            text_output = "\n---\n".join(text_parts) + "\n"

        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": text_output,
        }
        if as_json:
            cli_result["json"] = {"data": specs}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Export failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: command-level envelope mapping requires explicit text/json branches
def delete_command(
    persona_id: str,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.delete(persona_id)
    if isinstance(result, Success):
        payload: DeletedPersona = result.unwrap()
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("delete", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Delete failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: confirmation validation + envelope mapping requires explicit branches
def clear_command(
    confirm: str,
    *,
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    if confirm != CLEAR_CONFIRMATION_TOKEN:
        error_envelope: JsonErrorEnvelope = {
            "code": "INVALID_CONFIRMATION_TOKEN",
            "numeric_code": 112,  # INVALID_CONFIRMATION_TOKEN numeric code
            "message": "clear requires exact confirmation token 'CLEAR REGISTRY'",
            "details": {},
        }
        failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
        if not as_json:
            failure["stderr"] = f"Clear failed: {error_envelope['message']}\n"
        return Failure(failure)

    result = facade.clear(confirm)
    if isinstance(result, Success):
        payload: ClearedRegistry = result.unwrap()
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("clear", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Clear failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: command-level envelope mapping requires explicit text/json branches
def update_command(
    persona_id: str,
    *,
    patches: dict[str, object],
    as_json: bool,
    facade: LarvaFacade,
) -> Result[CliCommandResult, CliFailure]:
    result = facade.update(persona_id, patches=patches)
    if isinstance(result, Success):
        payload = dict(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("update", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope = _map_facade_error(result.failure())
    failure: CliFailure = {"exit_code": EXIT_ERROR, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Update failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: command-level envelope mapping requires explicit text/json branches
def component_list_command(
    *, as_json: bool, component_store: ComponentStore
) -> Result[CliCommandResult, CliFailure]:
    try:
        result = component_store.list_components()
    except Exception as error:
        error_envelope, exit_code = _map_component_error(error)
        failure: CliFailure = {"exit_code": exit_code, "error": error_envelope}
        if not as_json:
            failure["stderr"] = f"Component list failed: {error_envelope['message']}\n"
        return Failure(failure)

    if isinstance(result, Success):
        payload = dict(result.unwrap())
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("component list", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope, exit_code = _map_component_error(result.failure())
    failure: CliFailure = {"exit_code": exit_code, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Component list failed: {error_envelope['message']}\n"
    return Failure(failure)


# @shell_complexity: target parsing + loader routing requires explicit branches
def component_show_command(
    component_ref: str,
    *,
    as_json: bool,
    component_store: ComponentStore,
) -> Result[CliCommandResult, CliFailure]:
    component_type, separator, component_name = component_ref.partition("/")
    if separator == "" or component_type == "" or component_name == "":
        failure = _component_show_invalid_target(component_ref)
        if not as_json:
            error_envelope = failure.get("error", _critical_error("unknown error"))
            failure["stderr"] = f"Component show failed: {error_envelope['message']}\n"
        return Failure(failure)

    loaders: dict[str, Callable[[str], Result[object, object]]] = {
        "prompts": cast("Callable[[str], Result[object, object]]", component_store.load_prompt),
        "toolsets": cast("Callable[[str], Result[object, object]]", component_store.load_toolset),
        "constraints": cast(
            "Callable[[str], Result[object, object]]", component_store.load_constraint
        ),
        "models": cast("Callable[[str], Result[object, object]]", component_store.load_model),
    }
    loader = loaders.get(component_type)
    if loader is None:
        failure = _component_show_invalid_target(component_ref, component_type=component_type)
        if not as_json:
            error_envelope = failure.get("error", _critical_error("unknown error"))
            failure["stderr"] = f"Component show failed: {error_envelope['message']}\n"
        return Failure(failure)

    try:
        load_result = loader(component_name)
    except Exception as error:
        error_envelope, exit_code = _map_component_error(error)
        failure: CliFailure = {"exit_code": exit_code, "error": error_envelope}
        if not as_json:
            failure["stderr"] = f"Component show failed: {error_envelope['message']}\n"
        return Failure(failure)

    if isinstance(load_result, Success):
        payload = cast("dict[str, object]", dict(cast("dict[str, object]", load_result.unwrap())))
        cli_result: CliCommandResult = {
            "exit_code": EXIT_OK,
            "stdout": _render_payload_for_text("component show", payload),
        }
        if as_json:
            cli_result["json"] = {"data": payload}
        return Success(cli_result)

    error_envelope, exit_code = _map_component_error(load_result.failure())
    failure: CliFailure = {"exit_code": exit_code, "error": error_envelope}
    if not as_json:
        failure["stderr"] = f"Component show failed: {error_envelope['message']}\n"
    return Failure(failure)
