"""
CLI orchestration helpers - extracted from cli.py for testability.

These functions handle CLI orchestration workflows (locking, config loading,
output formatting) while staying in the Shell layer (I/O operations).

Reference: Gate Review - function size reduction requirement
"""

# @invar:allow shell_result: Module-level helpers use None|value returns for CLI error handling
# @invar:allow shell_pure_logic: Module uses fcntl and typer.echo, I/O operations flagged as pure
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any, TextIO

import typer
from returns.result import Failure, Success

if TYPE_CHECKING:
    pass


# @invar:allow shell_result: CLI output formatting, not I/O
# @invar:allow shell_pure_logic: String formatting helper
def format_project_list(infos: list) -> str:
    """Format project list for output.

    >>> format_project_list([])
    '\\nName                 Status     Path\\n--------------------------------------------------------------------------------'
    """
    lines = [f"\n{'Name':<20} {'Status':<10} {'Path'}", "-" * 80]
    for info in infos:
        status = "✓ valid" if info.exists else "✗ missing"
        lines.append(f"{info.name:<20} {status:<10} {info.path}")
    return "\n".join(lines)


# @invar:allow shell_result: CLI output formatting, not I/O
# @invar:allow shell_pure_logic: String formatting helper
def format_verify_results(results: list) -> str:
    """Format verify results for output.

    >>> format_verify_results([])
    ''
    >>> format_verify_results([("proj", True, "ok")])
    '✓ proj: ok'
    """
    return "\n".join(f"{'✓' if ok else '✗'} {name}: {msg}" for name, ok, msg in results)


# @invar:allow shell_result: acquire_global_evolution_lock returns (fd, path)|None, not Result
# @invar:allow shell_pure_logic: acquire_global_evolution_lock uses fcntl flock I/O operation
def acquire_global_evolution_lock(
    global_path: Path,
) -> tuple[TextIO, Path] | None:
    """Acquire global evolution lock.

    Returns (lock_fd, lock_path) on success, None on failure (prints error).

    >>> from pathlib import Path
    >>> import tempfile
    >>> with tempfile.TemporaryDirectory() as tmpdir:
    ...     result = acquire_global_evolution_lock(Path(tmpdir))
    ...     result is not None
    True
    """
    import fcntl
    import sys

    lock_path = global_path / "evolve.lock"
    lock_fd = None

    # Ensure global directory exists
    global_path.mkdir(parents=True, exist_ok=True)

    try:
        lock_fd = open(lock_path, "w")
        fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
        lock_fd.write(f"{sys.argv[0]}\n")
        lock_fd.flush()
        return lock_fd, lock_path
    except BlockingIOError:
        typer.secho(
            "Another global evolution is in progress. Try again later.",
            fg=typer.colors.YELLOW,
        )
        return None
    except OSError as e:
        typer.secho(f"Error acquiring lock: {e}", fg=typer.colors.RED)
        return None


# @invar:allow shell_pure_logic: release_global_evolution_lock uses fcntl flock I/O operation
def release_global_evolution_lock(lock_fd: TextIO | None) -> None:
    """Release global evolution lock.

    >>> release_global_evolution_lock(None)  # Does nothing with None
    """
    import fcntl

    if lock_fd is not None:
        fcntl.flock(lock_fd.fileno(), fcntl.LOCK_UN)
        lock_fd.close()


# @invar:allow shell_result: load_global_config returns (config, model)|None, not Result
def load_global_config(global_path: Path) -> tuple[Any, Any] | None:
    """Load global config, returning (config, model) or None on error."""
    from lattice.core.config import get_default_config
    from lattice.shell.config import read_config

    global_config_path = global_path / "config.toml"
    if global_config_path.exists():
        config_result = read_config(global_config_path)
        if isinstance(config_result, Failure):
            typer.secho(
                f"Error reading global config: {config_result.failure()}",
                fg=typer.colors.RED,
            )
            return None
        config = config_result.unwrap()
    else:
        config = get_default_config()

    if not config.compiler.model:
        typer.secho(
            "No LLM model configured. Set [compiler].model in ~/.lattice/config.toml",
            fg=typer.colors.RED,
        )
        return None

    return config, config.compiler.model


def print_evolution_summary(
    phase1_output: Any,
    phase2_output: Any,
    converged_proposals: list[Any],
) -> None:
    """Print evolution summary."""
    typer.secho("\n✓ Global evolution complete", fg=typer.colors.GREEN)
    typer.secho(
        f"  Candidates: {len(phase1_output.candidates)}",
        fg=typer.colors.CYAN,
    )
    typer.secho(
        f"  Proposals: {len(phase2_output.proposals)}",
        fg=typer.colors.CYAN,
    )
    typer.secho(
        f"  Converged (≥3): {len(converged_proposals)}",
        fg=typer.colors.CYAN,
    )

    if converged_proposals:
        typer.secho("\nConverged proposals:", fg=typer.colors.YELLOW)
        for i, prop in enumerate(converged_proposals, 1):
            typer.secho(
                f"  [{i}] {prop.action}: {prop.title} ({len(prop.source_projects)} projects)",
                fg=typer.colors.YELLOW,
            )
        typer.secho(
            "\nTo apply: lattice apply --global <proposal>",
            fg=typer.colors.YELLOW,
        )


# @invar:allow shell_complexity: Global evolution post-processing with multi-step writes
# @shell_complexity: Global evolution post-processing with multi-step writes
def write_converged_proposals(
    converged_proposals: list[Any],
    phase1_output: Any,
    phase2_output: Any,
    project_infos: list[Any],
    projects_result: Any,
    global_path: Path,
) -> None:
    """Write converged proposals to global.db and mark promoted rules."""
    from lattice.shell.global_evolution import (
        EvidenceRecord,
        write_evidence_to_global_db,
        write_rule_evidence_associations,
        mark_rule_promoted,
        write_global_evolution_output,
    )

    typer.secho("\n[Post-processing] Writing to global.db...", fg=typer.colors.CYAN)

    evidence_records: list[EvidenceRecord] = []
    target_to_indices: dict[str, list[int]] = {}
    for proposal in converged_proposals:
        if proposal.target not in target_to_indices:
            target_to_indices[proposal.target] = []
        for proj_name in proposal.source_projects:
            target_to_indices[proposal.target].append(len(evidence_records))
            evidence_records.append(
                EvidenceRecord(
                    source_project=proj_name,
                    source_session_id=",".join(proposal.evidence_sessions),
                    source_rule_file=proposal.target,
                    pattern=proposal.title,
                    summary=proposal.rationale,
                    original_snippet=proposal.content,
                    confidence=0.9 if proposal.action == "ADD" else 0.7,
                )
            )

    if evidence_records:
        write_result = write_evidence_to_global_db(evidence_records)
        if isinstance(write_result, Failure):
            typer.secho(
                f"Warning: Failed to write evidence: {write_result.failure()}",
                fg=typer.colors.YELLOW,
            )
        else:
            # Wire rule_evidence associations
            evidence_rowids = write_result.unwrap()
            for target_rule, indices in target_to_indices.items():
                rule_evidence_ids = [
                    evidence_rowids[i] for i in indices if i < len(evidence_rowids)
                ]
                if rule_evidence_ids:
                    assoc_result = write_rule_evidence_associations(
                        target_rule, rule_evidence_ids
                    )
                    if isinstance(assoc_result, Failure):
                        typer.secho(
                            f"Warning: Failed to write associations for {target_rule}: {assoc_result.failure()}",
                            fg=typer.colors.YELLOW,
                        )

    typer.secho(
        f"  Evidence records written: {len(evidence_records)}",
        fg=typer.colors.CYAN,
    )

    # Mark promoted rules
    project_paths: dict[str, Path] = {}
    if isinstance(projects_result, Success):
        for proj in projects_result.unwrap():
            project_paths[proj.name] = Path(proj.path)

    marked_count = 0
    for proposal in converged_proposals:
        for proj_name in proposal.source_projects:
            if proj_name in project_paths:
                mark_result = mark_rule_promoted(
                    project_paths[proj_name], proposal.target
                )
                if isinstance(mark_result, Success):
                    marked_count += 1

    if marked_count > 0:
        typer.secho(
            f"  Rules marked as promoted: {marked_count}",
            fg=typer.colors.CYAN,
        )

    # Write trace and proposals
    write_result = write_global_evolution_output(
        phase1_cot=phase1_output.cot_output,
        phase2_cot=phase2_output.cot_output,
        proposals=converged_proposals,
        projects_scanned=len(project_infos),
        global_path=global_path,
    )
    if isinstance(write_result, Success):
        proposal_path, trace_path = write_result.unwrap()
        typer.secho(
            f"  Trace written: {trace_path}",
            fg=typer.colors.CYAN,
        )
        typer.secho(
            f"  Proposals written: {proposal_path}",
            fg=typer.colors.CYAN,
        )
    else:
        typer.secho(
            f"  Warning: Failed to write trace/proposals: {write_result.failure()}",
            fg=typer.colors.YELLOW,
        )
