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

from __future__ import annotations

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

from ductor_bot.cli.codex_events import parse_codex_jsonl
from ductor_bot.cli.gemini_events import parse_gemini_json
from ductor_bot.cli.gemini_utils import find_gemini_cli
from ductor_bot.cli.param_resolver import TaskExecutionConfig
from ductor_bot.infra.process_tree import force_kill_process_tree

logger = logging.getLogger(__name__)

_IS_WINDOWS = sys.platform == "win32"

# 0x08000000 on Windows prevents a console window from appearing.
_CREATION_FLAGS: int = getattr(subprocess, "CREATE_NO_WINDOW", 0) if _IS_WINDOWS else 0


@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 ductor.\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)
    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 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"]

    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)


_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,
}

_RESULT_PARSERS: dict[str, _ResultParser] = {
    "claude": parse_claude_result,
    "gemini": parse_gemini_result,
    "codex": parse_codex_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
    cmd: list[str],
    *,
    cwd: Path,
    provider: str,
    timeout_seconds: float,
    timeout_label: str,
    stdin_input: bytes | None = None,
) -> OneShotExecutionResult:
    """Run one provider CLI command with timeout and normalized status/result."""
    proc = await asyncio.create_subprocess_exec(
        *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,
        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,
    )
