"""Run execution and stream-capture helpers extracted from `hiveos.observe`."""

from __future__ import annotations

import os
import queue
import shlex
import shutil
import subprocess
import sys
import threading


def _reader_thread(stream, label, out_queue, *, shorten):
    try:
        for line in iter(stream.readline, ""):
            out_queue.put((label, line))
    except Exception as exc:
        out_queue.put((label, f"[stream_decode_error] {shorten(exc, limit=240)}\n"))
    finally:
        out_queue.put((label, None))


def _is_windows():
    return os.name == "nt"


def _is_shell_wrapper_command(command_name):
    name = str(command_name or "").strip().lower()
    return name in {"cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe"}


def _format_windows_cmd_fallback(argv):
    return f"cmd /c {subprocess.list2cmdline(list(argv or []))}".strip()


def _format_launch_error(argv, exc):
    parts = [str(part) for part in (argv or []) if str(part)]
    executable = str(parts[0] if parts else "").strip()
    base = f"Executable not found: {executable or '<unknown>'}"
    resolved = shutil.which(executable) if executable else None
    if resolved:
        base = f"{base} (resolved to {resolved})"
    details = str(exc or "").strip()
    if details:
        base = f"{base}. {details}"
    if _is_windows() and executable and (not _is_shell_wrapper_command(executable)):
        base += f"\nTry: hive trace run -- {_format_windows_cmd_fallback(parts)}"
    return base


def safe_stream_write(target, text):
    try:
        target.write(text)
        target.flush()
        return
    except UnicodeEncodeError:
        pass
    try:
        encoding = str(getattr(target, "encoding", "") or "utf-8")
        data = str(text or "").encode(encoding, errors="replace")
        buffer = getattr(target, "buffer", None)
        if buffer is not None:
            buffer.write(data)
            buffer.flush()
            return
    except Exception:
        pass
    try:
        target.write(str(text or "").encode("utf-8", errors="replace").decode("utf-8", errors="replace"))
        target.flush()
    except Exception:
        return


def stream_process_output(
    proc,
    run_id,
    command_text,
    agent_id,
    *,
    now_ms,
    shorten,
    emit_event,
    heartbeat_cb=None,
    timeout_sec=None,
    started_at_ms=None,
):
    out_queue = queue.Queue()
    stdout_thread = threading.Thread(
        target=_reader_thread, args=(proc.stdout, "stdout", out_queue), kwargs={"shorten": shorten}, daemon=True
    )
    stderr_thread = threading.Thread(
        target=_reader_thread, args=(proc.stderr, "stderr", out_queue), kwargs={"shorten": shorten}, daemon=True
    )
    stdout_thread.start()
    stderr_thread.start()
    closed = set()
    seq = 0
    started_ms = int(started_at_ms or now_ms())
    timeout_window_ms = int(float(timeout_sec or 0) * 1000) if timeout_sec and float(timeout_sec) > 0 else 0
    while len(closed) < 2:
        if timeout_window_ms > 0 and (now_ms() - started_ms) >= timeout_window_ms:
            raise subprocess.TimeoutExpired(command_text, timeout_sec)
        try:
            channel, chunk = out_queue.get(timeout=0.25)
        except queue.Empty:
            if heartbeat_cb is not None:
                try:
                    heartbeat_cb()
                except Exception:
                    pass
            if proc.poll() is not None and (not stdout_thread.is_alive()) and (not stderr_thread.is_alive()):
                break
            continue
        if chunk is None:
            closed.add(channel)
            continue
        seq += 1
        if heartbeat_cb is not None:
            try:
                heartbeat_cb(last_output_ms=now_ms())
            except Exception:
                pass
        if channel == "stdout":
            safe_stream_write(sys.stdout, chunk)
        else:
            safe_stream_write(sys.stderr, chunk)
        emit_event(
            run_id=run_id,
            event_type="observe_output",
            outcome="success",
            payload={
                "step_name": "command.output",
                "command": command_text,
                "stream": channel,
                "seq": seq,
                "text": shorten(chunk.rstrip("\n"), limit=2000),
                "canonical_anchor_type": "output.commit",
                "idempotency_hint": "low",
                "side_effect": False,
                "external_write": False,
                "source": "cli",
            },
            agent_id=agent_id,
        )
    stdout_thread.join(timeout=0.1)
    stderr_thread.join(timeout=0.1)


def run_command_observed(
    command,
    *,
    run_id=None,
    run_name=None,
    cwd=None,
    timeout_sec=None,
    agent_id="observer.cli",
    open_ui_url="",
    env_overrides=None,
    record_fields=None,
    build_run_id,
    now_ms,
    active_trace_log_path,
    persist_run_record,
    touch_run_record,
    emit_event,
    open_run_in_ui,
    shorten,
):
    if isinstance(command, str):
        command_text = command.strip()
        if not command_text:
            raise ValueError("command must be non-empty")
        argv = shlex.split(command_text, posix=(os.name != "nt"))
    else:
        argv = [str(part) for part in (command or []) if str(part)]
        command_text = " ".join(argv).strip()
    if not argv:
        raise ValueError("command must be non-empty")

    rid = str(run_id or build_run_id("observe-run"))
    started_at = now_ms()
    record = {
        "run_id": rid,
        "run_name": str(run_name or command_text),
        "command": command_text,
        "argv": list(argv),
        "cwd": str(cwd or ""),
        "trace_log_path": active_trace_log_path(),
        "archived": False,
        "archived_at_ms": None,
        "archive_reason": None,
        "archive_actor": None,
        "started_at_ms": started_at,
        "last_update_ms": started_at,
        "last_output_ms": None,
        "status": "running",
        "exit_code": None,
        "duration_ms": None,
        "source": "cli",
    }
    record.update(dict(record_fields or {}))
    persist_run_record(record)
    emit_event(
        run_id=rid,
        event_type="observe_run_started",
        outcome="success",
        payload={
            "run_name": record["run_name"],
            "command": command_text,
            "cwd": record["cwd"],
            "source": "cli",
            **{
                key: value
                for key, value in (dict(record_fields or {})).items()
                if key in {"replay_of_run_id"}
            },
        },
        agent_id=agent_id,
    )
    emit_event(
        run_id=rid,
        event_type="observe_step",
        outcome="success",
        payload={
            "step_name": "command.start",
            "command": command_text,
            "boundary_type": "step.generic",
            "idempotency_hint": "unknown",
            "side_effect": False,
            "external_write": False,
            "source": "cli",
        },
        agent_id=agent_id,
    )

    env = {**os.environ, **dict(env_overrides or {})}
    launch_argv = list(argv)
    used_windows_shell_fallback = False
    launch_error = ""

    try:
        proc = subprocess.Popen(
            launch_argv,
            cwd=cwd or None,
            env=env,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            errors="replace",
            bufsize=1,
            universal_newlines=True,
            shell=False,
        )
    except FileNotFoundError as first_exc:
        can_try_windows_fallback = _is_windows() and bool(launch_argv) and (not _is_shell_wrapper_command(launch_argv[0]))
        if can_try_windows_fallback:
            fallback_argv = ["cmd", "/c", subprocess.list2cmdline(launch_argv)]
            try:
                proc = subprocess.Popen(
                    fallback_argv,
                    cwd=cwd or None,
                    env=env,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True,
                    errors="replace",
                    bufsize=1,
                    universal_newlines=True,
                    shell=False,
                )
                used_windows_shell_fallback = True
            except FileNotFoundError as second_exc:
                launch_error = _format_launch_error(launch_argv, second_exc)
        else:
            launch_error = _format_launch_error(launch_argv, first_exc)

    if launch_error:
        elapsed_ms = max(0, now_ms() - started_at)
        safe_stream_write(sys.stderr, launch_error + "\n")
        emit_event(
            run_id=rid,
            event_type="observe_output",
            outcome="failed",
            payload={
                "step_name": "command.output",
                "command": command_text,
                "stream": "stderr",
                "seq": 1,
                "text": shorten(launch_error, limit=2000),
                "canonical_anchor_type": "output.commit",
                "idempotency_hint": "low",
                "side_effect": False,
                "external_write": False,
                "source": "cli",
            },
            agent_id=agent_id,
        )
        emit_event(
            run_id=rid,
            event_type="observe_step",
            outcome="failed",
            payload={
                "step_name": "command.finish",
                "duration_ms": elapsed_ms,
                "exit_code": 127,
                "timeout_sec": None,
                "error": "executable_not_found",
                "canonical_anchor_type": "output.commit",
                "idempotency_hint": "low",
                "side_effect": False,
                "external_write": False,
                "source": "cli",
            },
            agent_id=agent_id,
        )
        emit_event(
            run_id=rid,
            event_type="observe_run_finished",
            outcome="failed",
            payload={
                "run_name": record["run_name"],
                "command": command_text,
                "duration_ms": elapsed_ms,
                "exit_code": 127,
                "error": "executable_not_found",
                "source": "cli",
                **{
                    key: value
                    for key, value in (dict(record_fields or {})).items()
                    if key in {"replay_of_run_id"}
                },
            },
            agent_id=agent_id,
        )
        record["status"] = "failed"
        record["exit_code"] = 127
        record["duration_ms"] = elapsed_ms
        record["finished_at_ms"] = now_ms()
        record["last_output_ms"] = now_ms()
        persist_run_record(record)
        if open_ui_url:
            open_run_in_ui(open_ui_url, rid)
        return {
            "run_id": rid,
            "exit_code": 127,
            "duration_ms": elapsed_ms,
            "status": "failed",
        }

    if used_windows_shell_fallback:
        emit_event(
            run_id=rid,
            event_type="observe_step",
            outcome="success",
            payload={
                "step_name": "command.launch_fallback",
                "mode": "windows_cmd_shell",
                "boundary_type": "step.generic",
                "idempotency_hint": "unknown",
                "side_effect": False,
                "external_write": False,
                "source": "cli",
            },
            agent_id=agent_id,
        )
        safe_stream_write(
            sys.stderr,
            "[hive trace] Windows launch fallback applied: using cmd /c for command shim compatibility.\n",
        )
    timed_out = False
    try:
        stream_process_output(
            proc,
            rid,
            command_text,
            agent_id,
            now_ms=now_ms,
            shorten=shorten,
            emit_event=emit_event,
            heartbeat_cb=lambda **fields: touch_run_record(rid, **fields),
            timeout_sec=timeout_sec,
            started_at_ms=started_at,
        )
        proc.wait(timeout=timeout_sec if timeout_sec and timeout_sec > 0 else None)
    except subprocess.TimeoutExpired:
        timed_out = True
        proc.kill()
        proc.wait(timeout=2)
    finally:
        try:
            if proc.stdout:
                proc.stdout.close()
        except Exception:
            pass
        try:
            if proc.stderr:
                proc.stderr.close()
        except Exception:
            pass

    elapsed_ms = max(0, now_ms() - started_at)
    exit_code = int(proc.returncode if proc.returncode is not None else 1)
    if timed_out:
        exit_code = 124
    ok = (not timed_out) and exit_code == 0
    outcome = "success" if ok else "failed"

    emit_event(
        run_id=rid,
        event_type="observe_step",
        outcome=outcome,
        payload={
            "step_name": "command.finish",
            "duration_ms": elapsed_ms,
            "exit_code": exit_code,
            "timeout_sec": timeout_sec if timed_out else None,
            "canonical_anchor_type": "output.commit",
            "idempotency_hint": "low",
            "side_effect": False,
            "external_write": False,
            "source": "cli",
        },
        agent_id=agent_id,
    )
    emit_event(
        run_id=rid,
        event_type="observe_run_finished",
        outcome=outcome,
        payload={
            "run_name": record["run_name"],
            "command": command_text,
            "duration_ms": elapsed_ms,
            "exit_code": exit_code,
            "error": "timeout_expired" if timed_out else "",
            "source": "cli",
            **{
                key: value
                for key, value in (dict(record_fields or {})).items()
                if key in {"replay_of_run_id"}
            },
        },
        agent_id=agent_id,
    )
    record["status"] = "success" if ok else "failed"
    record["exit_code"] = exit_code
    record["duration_ms"] = elapsed_ms
    record["finished_at_ms"] = now_ms()
    persist_run_record(record)

    if open_ui_url:
        open_run_in_ui(open_ui_url, rid)
    return {
        "run_id": rid,
        "exit_code": exit_code,
        "duration_ms": elapsed_ms,
        "status": record["status"],
    }

