"""Codex process launcher and stream parser (skeleton)."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import IO, Callable, Iterable, Iterator, Mapping
import errno
import json
import os
import subprocess
import sys


@dataclass
class CodexInvocation:
    """Command and environment prepared for invoking codex."""

    args: list[str]
    env: Mapping[str, str]
    workdir: Path
    prompt: str = ""  # Prompt content (sent via stdin for initial exec)
    exit_code: int | None = None
    stderr_output: str = ""  # Captured stderr for debugging
    prompt_file: Path | None = None
    stdin_handle: IO[str] | None = None
    input_writer: Callable[[str], None] | None = None
    terminate: Callable[[], None] | None = None
    use_pty: bool = False


@dataclass
class CodexEvent:
    """Structured event emitted by codex --json."""

    raw: dict[str, object]


def _parse_output_line(raw_line: str) -> dict[str, object]:
    line = raw_line.rstrip("\r")
    if not line.strip():
        return {}
    try:
        parsed = json.loads(line)
    except json.JSONDecodeError:
        return {"terminal_output": line}
    if isinstance(parsed, dict):
        return parsed
    return {"event": parsed}


def _write_to_pipe(handle: IO[str], text: str) -> None:
    handle.write(text)
    handle.write("\n")
    handle.flush()


def _write_to_pty(master_fd: int, text: str) -> None:
    payload = f"{text}\n".encode("utf-8")
    os.write(master_fd, payload)


def _launch_with_pipe(invocation: CodexInvocation) -> subprocess.Popen[str]:
    proc = subprocess.Popen(
        invocation.args,
        cwd=invocation.workdir,
        env=invocation.env if invocation.env else None,
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        encoding="utf-8",  # Explicit UTF-8 for cross-platform Unicode support
        bufsize=1,
    )
    assert proc.stdin is not None
    assert proc.stdout is not None
    pipe_stdin = proc.stdin
    invocation.stdin_handle = proc.stdin
    invocation.input_writer = lambda text: _write_to_pipe(pipe_stdin, text)
    invocation.terminate = proc.terminate
    return proc


def _launch_with_pty(
    invocation: CodexInvocation,
) -> tuple[subprocess.Popen[bytes], int]:
    import pty

    master_fd, slave_fd = pty.openpty()
    try:
        proc = subprocess.Popen(
            invocation.args,
            cwd=invocation.workdir,
            env=invocation.env if invocation.env else None,
            stdin=slave_fd,
            stdout=slave_fd,
            stderr=slave_fd,
            text=False,
            bufsize=0,
        )
    except Exception:
        os.close(master_fd)
        os.close(slave_fd)
        raise

    os.close(slave_fd)
    invocation.stdin_handle = None
    invocation.input_writer = lambda text: _write_to_pty(master_fd, text)
    invocation.terminate = proc.terminate
    return proc, master_fd


def _stream_pipe_output(
    invocation: CodexInvocation, proc: subprocess.Popen[str]
) -> Iterator[CodexEvent]:
    assert proc.stdout is not None

    for raw_line in proc.stdout:
        line = raw_line.rstrip("\r\n")
        parsed = _parse_output_line(line)
        if parsed:
            yield CodexEvent(raw=parsed)

    proc.wait()
    invocation.exit_code = proc.returncode
    invocation.stdin_handle = None
    invocation.input_writer = None
    invocation.terminate = None

    # Always capture stderr for debugging
    stderr_content = ""
    if proc.stderr:
        stderr_content = proc.stderr.read().strip()
        invocation.stderr_output = stderr_content


def _stream_pty_output(
    invocation: CodexInvocation,
    proc: subprocess.Popen[bytes],
    master_fd: int,
) -> Iterator[CodexEvent]:
    buffer = bytearray()
    try:
        while True:
            try:
                chunk = os.read(master_fd, 4096)
            except OSError as exc:
                # PTYs can return EIO on EOF after child exit.
                if exc.errno == errno.EIO:
                    break
                raise

            if not chunk:
                break

            buffer.extend(chunk)
            while True:
                newline_index = buffer.find(b"\n")
                if newline_index < 0:
                    break
                raw_line = buffer[:newline_index]
                del buffer[: newline_index + 1]
                line = raw_line.decode("utf-8", errors="replace")
                parsed = _parse_output_line(line)
                if parsed:
                    yield CodexEvent(raw=parsed)

        if buffer:
            line = bytes(buffer).decode("utf-8", errors="replace")
            parsed = _parse_output_line(line)
            if parsed:
                yield CodexEvent(raw=parsed)

        proc.wait()
        invocation.exit_code = proc.returncode
    finally:
        invocation.stdin_handle = None
        invocation.input_writer = None
        invocation.terminate = None
        os.close(master_fd)


def build_invocation(
    codex_executable: Path,
    prompt: str,
    workdir: Path,
    env: Mapping[str, str],
    extra_flags: Iterable[str] = (),
) -> CodexInvocation:
    """Assemble the codex exec command.

    The prompt is written to a file in the workdir for debugging, then provided
    over stdin via the ``-`` prompt argument to avoid shell arg length limits.
    """
    # Write prompt to file in the workdir (which Codex has access to)
    prompt_file = workdir / "flywheel_prompt.txt"
    prompt_file.write_text(prompt, encoding="utf-8")

    # Use "-" so codex reads the actual prompt text from stdin.
    args = [
        str(codex_executable),
        "exec",
        "--json",
        "--cd",
        str(workdir),
        "--skip-git-repo-check",
        *list(extra_flags),
        "-",
    ]
    return CodexInvocation(
        args=args,
        env=dict(env),
        workdir=workdir,
        prompt=prompt,
        prompt_file=prompt_file,
        use_pty=False,
    )


def build_resume_invocation(
    codex_executable: Path,
    session_id: str,
    prompt: str,
    workdir: Path,
    env: Mapping[str, str],
    extra_flags: Iterable[str] = (),
) -> CodexInvocation:
    """Assemble ``codex exec resume --json`` command."""
    args = [
        str(codex_executable),
        "exec",
        "resume",
        "--json",
        "--skip-git-repo-check",
        *list(extra_flags),
        session_id,
        prompt,
    ]
    return CodexInvocation(
        args=args,
        env=dict(env),
        workdir=workdir,
        prompt=prompt,
        use_pty=False,
    )


def run_and_stream(invocation: CodexInvocation) -> Iterator[CodexEvent]:
    """Launch codex and yield parsed events as they arrive.

    For initial ``codex exec`` invocations built by ``build_invocation``, the
    prompt text is written once to stdin and then stdin is closed.
    """
    # Log the command being run (redact prompt for brevity)
    cmd_display = " ".join(invocation.args)
    print(f"[bootstrap] Running: {cmd_display}", file=sys.stderr)
    print(f"[bootstrap] Workdir: {invocation.workdir}", file=sys.stderr)
    print(f"[bootstrap] Prompt length: {len(invocation.prompt)} chars", file=sys.stderr)

    # Emit command info as first event for server-side logging
    yield CodexEvent(
        raw={
            "bootstrap_debug": "codex_launch",
            "command": invocation.args,
            "workdir": str(invocation.workdir),
            "prompt_length": len(invocation.prompt),
        }
    )

    session_kind = "pty" if invocation.use_pty else "pipe"
    try:
        if invocation.use_pty and os.name != "nt":
            try:
                proc_pty, master_fd = _launch_with_pty(invocation)
                session_kind = "pty"
                yield from _stream_pty_output(invocation, proc_pty, master_fd)
            except Exception as e:
                warning_msg = f"PTY launch failed, falling back to pipes: {e}"
                print(f"[bootstrap] WARNING: {warning_msg}", file=sys.stderr)
                yield CodexEvent(raw={"bootstrap_warning": warning_msg})
                proc_pipe = _launch_with_pipe(invocation)
                yield from _stream_pipe_output(invocation, proc_pipe)
        else:
            proc_pipe = _launch_with_pipe(invocation)
            # For initial exec invocations that use "-" as prompt source, seed
            # stdin with the prepared prompt text and then close stdin so codex
            # can start processing immediately.
            if (
                invocation.prompt_file is not None
                and invocation.args
                and invocation.args[-1] == "-"
                and proc_pipe.stdin is not None
            ):
                try:
                    proc_pipe.stdin.write(invocation.prompt)
                    proc_pipe.stdin.close()
                    invocation.stdin_handle = None
                    invocation.input_writer = None
                except Exception as e:
                    warning_msg = f"Failed to write bootstrap prompt to stdin: {e}"
                    print(f"[bootstrap] WARNING: {warning_msg}", file=sys.stderr)
                    yield CodexEvent(raw={"bootstrap_warning": warning_msg})
            yield from _stream_pipe_output(invocation, proc_pipe)
    except Exception as e:
        error_msg = f"Failed to run codex process: {e}"
        print(f"[bootstrap] ERROR: {error_msg}", file=sys.stderr)
        yield CodexEvent(raw={"bootstrap_error": error_msg})
        invocation.exit_code = (
            invocation.exit_code if invocation.exit_code is not None else 127
        )

    exit_code = invocation.exit_code if invocation.exit_code is not None else 127
    stderr_content = invocation.stderr_output.strip()

    # Log exit status
    print(f"[bootstrap] Codex exited with code: {exit_code}", file=sys.stderr)
    if stderr_content:
        print(f"[bootstrap] Codex stderr:\n{stderr_content}", file=sys.stderr)

    # Emit detailed exit info
    yield CodexEvent(
        raw={
            "bootstrap_debug": "codex_exit",
            "exit_code": exit_code,
            "stderr": stderr_content if stderr_content else None,
            "session_kind": session_kind,
        }
    )
