from __future__ import annotations

import os
import platform
import sys
import time
from dataclasses import dataclass, field
from enum import Enum
from functools import partial
from multiprocessing import util as mp_util
from multiprocessing.connection import Connection
from pathlib import Path
from runpy import run_path
from threading import Lock
from typing import Any, Callable, Literal

from ferp.fscp.host import Host
from ferp.fscp.host.managed_process import WorkerFn
from ferp.fscp.host.process_registry import (
    ProcessMetadata,
    ProcessRegistry,
)
from ferp.fscp.protocol.messages import Message, MessageDirection, MessageType
from ferp.fscp.protocol.state import HostState
from ferp.fscp.scripts.runtime.io import configure_connection
from ferp.fscp.transcript.events import TranscriptEvent
from ferp.services.scripts import ScriptExecutionContext


def _patch_spawnv_passfds() -> None:
    """Work around macOS passing invalid fds to spawnv."""
    original_spawn = mp_util.spawnv_passfds

    def safe_spawn(exe, args, passfds):  # type: ignore[override]
        filtered = tuple(fd for fd in passfds if isinstance(fd, int) and fd >= 0)
        return original_spawn(exe, args, filtered)

    mp_util.spawnv_passfds = safe_spawn  # type: ignore[assignment]


_patch_spawnv_passfds()


def _read_app_version() -> str:
    try:
        from ferp.__version__ import __version__
    except Exception:
        return "unknown"
    return __version__


def _read_build_label() -> str:
    if os.environ.get("FERP_DEV_CONFIG") == "1":
        return "dev"
    return "release"


def _read_os_version() -> str:
    if sys.platform == "darwin":
        mac_version = platform.mac_ver()[0]
        return mac_version or platform.release()
    if sys.platform.startswith("win"):
        return platform.version()
    return platform.release()


def _build_environment(app_root: Path, cache_dir: Path) -> dict[str, Any]:
    """Build the SDK environment payload for script initialization."""
    return {
        "app": {
            "name": "ferp",
            "version": _read_app_version(),
            "build": _read_build_label(),
        },
        "host": {
            "platform": sys.platform,
            "os": platform.system(),
            "os_version": _read_os_version(),
            "arch": platform.machine(),
            "python": platform.python_version(),
        },
        "paths": {
            "app_root": str(app_root),
            "cwd": str(Path.cwd()),
            "cache_dir": str(cache_dir),
        },
    }


class ScriptStatus(Enum):
    COMPLETED = "completed"
    FAILED = "failed"
    WAITING_INPUT = "waiting_input"


@dataclass(frozen=True)
class ScriptInputRequest:
    id: str
    prompt: str
    default: str | None = None
    secret: bool = False
    mode: Literal["input", "confirm"] = "input"
    fields: list[dict[str, Any]] = field(default_factory=list)
    suggestions: list[str] = field(default_factory=list)
    show_text_input: bool = True


@dataclass(frozen=True)
class ScriptResult:
    status: ScriptStatus
    transcript: list[TranscriptEvent] = field(default_factory=list)
    results: list[dict[str, Any]] = field(default_factory=list)
    exit_code: int | None = None
    error: str | None = None
    input_request: ScriptInputRequest | None = None


@dataclass
class HostSession:
    context: ScriptExecutionContext
    host: Host
    pending_request: ScriptInputRequest | None = None


def _script_worker_entry(script_path: str, app_root: str, conn: Connection) -> None:
    os.chdir(app_root)
    configure_connection(conn)
    run_path(script_path, run_name="__main__")


class ScriptRunner:
    """Run FSCP-compatible scripts inside a managed Host."""

    _TERMINAL_STATES = {
        HostState.TERMINATED,
        HostState.ERR_PROTOCOL,
        HostState.ERR_TRANSPORT,
    }

    def __init__(
        self,
        app_root: Path,
        cache_dir: Path,
        progress_handler: Callable[[dict[str, Any]], None] | None = None,
        process_registry: ProcessRegistry | None = None,
    ) -> None:
        self.app_root = app_root
        self.cache_dir = cache_dir
        self._session: HostSession | None = None
        self._lock = Lock()
        self._progress_handler = progress_handler
        self.process_registry = process_registry or ProcessRegistry()

    @property
    def active_script_name(self) -> str | None:
        session = self._session
        if session is None:
            return None
        return session.context.script.name

    @property
    def active_target(self) -> Path | None:
        session = self._session
        if session is None:
            return None
        return session.context.target_path

    @property
    def active_process_handle(self) -> str | None:
        session = self._session
        if session is None:
            return None
        return session.host.process_handle

    def start(self, context: ScriptExecutionContext) -> ScriptResult:
        worker = self._create_worker(context.script_path)
        metadata = ProcessMetadata(
            script_name=context.script.name,
            script_id=context.script.id,
            target_path=context.target_path,
        )
        host = Host(
            worker=worker,
            process_registry=self.process_registry,
            process_metadata=metadata,
        )
        session = HostSession(context=context, host=host)

        with self._lock:
            if self._session is not None:
                raise RuntimeError("A script is already running.")
            self._session = session

        try:
            host.start()
            environment = _build_environment(self.app_root, self.cache_dir)
            init_payload = {
                "target": {
                    "path": str(context.target_path),
                    "kind": context.target_kind,
                },
                "params": {
                    "script": {
                        "id": context.script.id,
                        "name": context.script.name,
                        "version": context.script.version,
                        "path": str(context.script_path),
                    },
                },
                "environment": environment,
            }
            host.send(Message(type=MessageType.INIT, payload=init_payload))
            return self._drive_host(session)
        except Exception:
            with self._lock:
                self._session = None
            host.shutdown(force=True)
            raise

    def provide_input(self, value: str) -> ScriptResult:
        with self._lock:
            session = self._require_session()
            request = session.pending_request
            if request is None:
                raise RuntimeError("No pending input request.")
            payload = {"id": request.id, "value": value}
            session.pending_request = None
            host = session.host

        host.provide_input(payload)
        return self._drive_host(session)

    def abort(
        self,
        reason: str | None = None,
        *,
        graceful_timeout: float = 2.0,
    ) -> ScriptResult | None:
        with self._lock:
            session = self._session
            if session is None:
                return None
            self._session = None

        host = session.host
        try:
            if graceful_timeout > 0:
                try:
                    host.request_cancel()
                except Exception:
                    pass

                deadline = time.time() + graceful_timeout
                while time.time() < deadline:
                    host.poll()
                    updates = host.drain_progress_updates()
                    if updates:
                        for payload in updates:
                            self._publish_progress(payload)
                    if host.state in self._TERMINAL_STATES:
                        break
                    time.sleep(0.05)

            if host.state not in self._TERMINAL_STATES:
                host.shutdown(force=True)
        finally:
            return ScriptResult(
                status=ScriptStatus.FAILED,
                transcript=list(host.transcript),
                results=list(host.results),
                error=reason or "Script canceled.",
            )

    def _publish_progress(self, payload: dict[str, Any]) -> None:
        handler = self._progress_handler
        if handler:
            handler(payload)

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _drive_host(self, session: HostSession) -> ScriptResult:
        host = session.host

        while True:
            host.poll()
            updates = host.drain_progress_updates()
            if updates:
                for payload in updates:
                    self._publish_progress(payload)

            if host.state is HostState.AWAITING_INPUT:
                request = self._extract_input_request(host)
                session.pending_request = request
                return ScriptResult(
                    status=ScriptStatus.WAITING_INPUT,
                    transcript=list(host.transcript),
                    results=list(host.results),
                    input_request=request,
                )

            if host.state in self._TERMINAL_STATES:
                return self._finalize(session)

            time.sleep(0.05)

    def _finalize(self, session: HostSession) -> ScriptResult:
        host = session.host
        transcript = list(host.transcript)
        results = list(host.results)
        exit_code = self._extract_exit_code(transcript)
        state = host.state

        success = state is HostState.TERMINATED and (exit_code in (None, 0))
        status = ScriptStatus.COMPLETED if success else ScriptStatus.FAILED
        error = None if success else self._derive_error_message(host, exit_code)

        self._cleanup_session()

        return ScriptResult(
            status=status,
            transcript=transcript,
            results=results,
            exit_code=exit_code,
            error=error,
        )

    def _cleanup_session(self) -> None:
        with self._lock:
            self._session = None

    def _require_session(self) -> HostSession:
        session = self._session
        if session is None:
            raise RuntimeError("No active FSCP session.")
        return session

    def _create_worker(self, script_path: Path) -> WorkerFn:
        return partial(
            _script_worker_entry,
            str(script_path),
            str(self.app_root),
        )

    def _extract_input_request(self, host: Host) -> ScriptInputRequest:
        for event in reversed(host.transcript):
            msg = event.message
            if msg and msg.type is MessageType.REQUEST_INPUT:
                payload = msg.payload or {}
                raw_id = payload.get("id")
                if raw_id is None:
                    break
                mode = str(payload.get("mode", "input"))
                if mode not in {"input", "confirm"}:
                    mode = "input"
                raw_fields = payload.get("fields")
                fields: list[dict[str, Any]] = []
                if isinstance(raw_fields, list):
                    for item in raw_fields:
                        if isinstance(item, dict):
                            fields.append(dict(item))
                raw_suggestions = payload.get("suggestions")
                suggestions: list[str] = []
                if isinstance(raw_suggestions, list):
                    for value in raw_suggestions:
                        if isinstance(value, str) and value:
                            suggestions.append(value)
                show_text_input = payload.get("show_text_input", True)
                if not isinstance(show_text_input, bool):
                    show_text_input = True
                return ScriptInputRequest(
                    id=str(raw_id),
                    prompt=str(payload.get("prompt", "")),
                    default=payload.get("default"),
                    secret=bool(payload.get("secret", False)),
                    mode=mode,  # type: ignore
                    fields=fields,
                    suggestions=suggestions,
                    show_text_input=show_text_input,
                )

        raise RuntimeError("FSCP host entered input state without payload.")

    def _extract_exit_code(
        self,
        transcript: list[TranscriptEvent],
    ) -> int | None:
        for event in reversed(transcript):
            msg = event.message
            if msg and msg.type is MessageType.EXIT:
                payload = msg.payload or {}
                code = payload.get("code")
                if isinstance(code, int):
                    return code
        return None

    def _derive_error_message(
        self,
        host: Host,
        exit_code: int | None,
    ) -> str:
        detail = self._latest_system_note(host)

        if host.state is HostState.ERR_PROTOCOL:
            base = "Script failed due to an FSCP protocol violation."
        elif host.state is HostState.ERR_TRANSPORT:
            base = "Script failed due to a transport error."
        else:
            base = "Script exited with errors."

        if exit_code not in (None, 0):
            base = f"{base} (exit code {exit_code})"

        return f"{base} {detail}".strip()

    def _latest_system_note(self, host: Host) -> str:
        for event in reversed(host.transcript):
            if event.direction is MessageDirection.INTERNAL and event.raw:
                return event.raw
        return ""
