"""Cron job CLI command building and output parsing."""

from __future__ import annotations

import asyncio
import json
import logging
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path
from shutil import which

from klir.cli.codex_events import parse_codex_jsonl
from klir.cli.gemini_events import parse_gemini_json
from klir.cli.gemini_utils import find_gemini_cli
from klir.cli.opencode_events import parse_opencode_json
from klir.cli.param_resolver import TaskExecutionConfig
from klir.infra.platform import CREATION_FLAGS as _CREATION_FLAGS
from klir.infra.process_tree import force_kill_process_tree

logger = logging.getLogger(__name__)


@dataclass(slots=True)
class OneShotCommand:
    """Command + optional stdin payload for one-shot execution."""

    cmd: list[str] = field(default_factory=list)
    stdin_input: bytes | None = None


def build_cmd(exec_config: TaskExecutionConfig, prompt: str) -> OneShotCommand | None:
    """Build a CLI command for one-shot cron execution."""
    builder = _CMD_BUILDERS.get(exec_config.provider, _build_claude_cmd)
    return builder(exec_config, prompt)


def enrich_instruction(instruction: str, task_folder: str) -> str:
    """Append memory file instructions to the agent instruction."""
    memory_file = f"{task_folder}_MEMORY.md"
    return (
        f"{instruction}\n\n"
        f"IMPORTANT:\n"
        f"- Read the {memory_file} file (it contains important information!)\n"
        f"- When finished, update {memory_file} with DATE + TIME and what you have done.\n"
        "- The final answer is delivered to Telegram automatically by klir.\n"
        "- Return only the user-facing result text.\n"
        "- Do not include transport/debug/tool confirmations "
        '(for example: "Message sent successfully").'
    )


def parse_claude_result(stdout: bytes) -> str:
    """Extract result text from Claude CLI JSON output."""
    if not stdout:
        return ""
    raw = stdout.decode(errors="replace").strip()
    if not raw:
        return ""
    try:
        data = json.loads(raw)
        return str(data.get("result", ""))
    except json.JSONDecodeError:
        return raw[:2000]


def parse_gemini_result(stdout: bytes) -> str:
    """Extract result text from Gemini CLI JSON output."""
    if not stdout:
        return ""
    raw = stdout.decode(errors="replace").strip()
    if not raw:
        return ""
    return parse_gemini_json(raw) or raw[:2000]


def parse_codex_result(stdout: bytes) -> str:
    """Extract result text from Codex CLI JSONL output."""
    if not stdout:
        return ""
    raw = stdout.decode(errors="replace").strip()
    if not raw:
        return ""
    result_text, thread_id, usage = parse_codex_jsonl(raw)
    # If the JSONL was successfully parsed (thread_id or usage present),
    # an empty result genuinely means no output — don't leak raw events.
    if result_text:
        return result_text
    if thread_id is not None or usage is not None:
        return ""
    return raw[:2000]


def parse_opencode_result(stdout: bytes) -> str:
    """Extract result text from OpenCode CLI JSON output."""
    if not stdout:
        return ""
    raw = stdout.decode(errors="replace").strip()
    if not raw:
        return ""
    result_text, _session_id, _usage = parse_opencode_json(raw)
    return result_text or raw[:2000]


def parse_result(provider: str, stdout: bytes) -> str:
    """Extract result text from provider-specific CLI output."""
    parser = _RESULT_PARSERS.get(provider, parse_claude_result)
    return parser(stdout)


def indent(text: str, prefix: str) -> str:
    """Indent every line of *text* with *prefix*."""
    return "\n".join(prefix + line for line in text.splitlines())


# -- Private builders --


def _build_claude_cmd(exec_config: TaskExecutionConfig, prompt: str) -> OneShotCommand | None:
    """Build a Claude CLI command for one-shot cron execution."""
    cli = which("claude")
    if not cli:
        return None
    cmd = [
        cli,
        "-p",
        "--output-format",
        "json",
        "--model",
        exec_config.model,
        "--permission-mode",
        exec_config.permission_mode,
        "--no-session-persistence",
    ]
    # Add tool filtering
    if exec_config.allowed_tools:
        cmd += ["--allowedTools", *exec_config.allowed_tools]
    if exec_config.disallowed_tools:
        cmd += ["--disallowedTools", *exec_config.disallowed_tools]
    # Add extra CLI parameters
    cmd.extend(exec_config.cli_parameters)
    cmd += ["--", prompt]
    return OneShotCommand(cmd=cmd)


def _build_gemini_cmd(exec_config: TaskExecutionConfig, prompt: str) -> OneShotCommand | None:
    """Build a Gemini CLI command for one-shot cron execution.

    Uses hybrid mode: ``-p ""`` forces headless mode (bypassing the TTY check
    that causes exit-42 on Windows), while the actual prompt is fed via stdin.
    """
    try:
        cli = find_gemini_cli()
    except FileNotFoundError:
        return None
    cmd = [cli, "-p", "", "--output-format", "json", "--include-directories", "."]

    if exec_config.model:
        cmd += ["--model", exec_config.model]
    if exec_config.permission_mode == "bypassPermissions":
        cmd += ["--approval-mode", "yolo"]

    # Add tool filtering (Gemini only supports allowed-tools)
    if exec_config.allowed_tools:
        cmd += ["--allowed-tools", *exec_config.allowed_tools]

    cmd.extend(exec_config.cli_parameters)
    return OneShotCommand(cmd=cmd, stdin_input=prompt.encode())


def _build_codex_cmd(exec_config: TaskExecutionConfig, prompt: str) -> OneShotCommand | None:
    """Build a Codex CLI command for one-shot cron execution."""
    cli = which("codex")
    if not cli:
        return None
    cmd = [cli, "exec", "--json", "--color", "never", "--skip-git-repo-check"]

    # Sandbox flags based on permission_mode
    if exec_config.permission_mode == "bypassPermissions":
        cmd.append("--dangerously-bypass-approvals-and-sandbox")
    else:
        cmd.append("--full-auto")

    cmd += ["--model", exec_config.model]

    # Add reasoning effort (if not default)
    if exec_config.reasoning_effort and exec_config.reasoning_effort != "medium":
        cmd += ["-c", f"model_reasoning_effort={exec_config.reasoning_effort}"]

    # Add extra CLI parameters
    cmd.extend(exec_config.cli_parameters)

    cmd += ["--", prompt]
    return OneShotCommand(cmd=cmd)


def _build_opencode_cmd(exec_config: TaskExecutionConfig, prompt: str) -> OneShotCommand | None:
    """Build an OpenCode CLI command for one-shot cron execution."""
    cli = which("opencode")
    if not cli:
        return None
    cmd = [cli, "-p", prompt, "-q", "-f", "json"]

    if exec_config.model:
        cmd += ["-m", exec_config.model]

    # Add tool filtering
    if exec_config.allowed_tools:
        cmd += ["--allowedTools", ",".join(exec_config.allowed_tools)]
    if exec_config.disallowed_tools:
        cmd += ["--excludedTools", ",".join(exec_config.disallowed_tools)]

    # Add extra CLI parameters
    cmd.extend(exec_config.cli_parameters)
    return OneShotCommand(cmd=cmd)


_CmdBuilder = Callable[[TaskExecutionConfig, str], OneShotCommand | None]
_ResultParser = Callable[[bytes], str]

_CMD_BUILDERS: dict[str, _CmdBuilder] = {
    "claude": _build_claude_cmd,
    "gemini": _build_gemini_cmd,
    "codex": _build_codex_cmd,
    "opencode": _build_opencode_cmd,
}

_RESULT_PARSERS: dict[str, _ResultParser] = {
    "claude": parse_claude_result,
    "gemini": parse_gemini_result,
    "codex": parse_codex_result,
    "opencode": parse_opencode_result,
}


@dataclass(slots=True)
class OneShotExecutionResult:
    """Normalized outcome for a one-shot provider subprocess run."""

    status: str
    result_text: str
    stdout: bytes
    stderr: bytes
    returncode: int | None
    timed_out: bool


def _force_kill(proc: asyncio.subprocess.Process) -> None:
    """Force-kill a subprocess and any descendants."""
    force_kill_process_tree(proc.pid)


async def execute_one_shot(  # noqa: PLR0913
    one_shot: OneShotCommand,
    *,
    cwd: Path,
    provider: str,
    timeout_seconds: float,
    timeout_label: str,
    extra_env: dict[str, str] | None = None,
) -> OneShotExecutionResult:
    """Run one provider CLI command with timeout and normalized status/result."""
    import os

    stdin_input = one_shot.stdin_input
    env: dict[str, str] | None = {**os.environ, **extra_env} if extra_env else None
    proc = await asyncio.create_subprocess_exec(
        *one_shot.cmd,
        cwd=str(cwd),
        stdin=asyncio.subprocess.PIPE if stdin_input is not None else asyncio.subprocess.DEVNULL,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        env=env,
        creationflags=_CREATION_FLAGS,
    )

    timed_out = False
    try:
        async with asyncio.timeout(timeout_seconds):
            stdout, stderr = await proc.communicate(input=stdin_input)
    except TimeoutError:
        timed_out = True
        _force_kill(proc)
        stdout, stderr = await proc.communicate()
    except asyncio.CancelledError:
        _force_kill(proc)
        await proc.wait()
        raise

    if timed_out:
        return OneShotExecutionResult(
            status="error:timeout",
            result_text=f"[{timeout_label} timed out after {timeout_seconds:.0f}s]",
            stdout=stdout,
            stderr=stderr,
            returncode=proc.returncode,
            timed_out=True,
        )

    returncode = proc.returncode
    status = "success" if returncode == 0 else f"error:exit_{returncode}"
    return OneShotExecutionResult(
        status=status,
        result_text=parse_result(provider, stdout),
        stdout=stdout,
        stderr=stderr,
        returncode=returncode,
        timed_out=False,
    )
