import json
import os
import subprocess
import time
import uuid
from threading import RLock

from hiveos.config import AI_OLLAMA_MODEL, AI_OLLAMA_TIMEOUT_SEC, AI_PROVIDER, get_ai_route_model
from hiveos.intelligence.tracing import build_trace_event, emit_trace


ALLOWED_TARGET_TYPES = {"software.pty", "hardware.bridge"}
ALLOWED_TRANSPORT_KINDS = {"pty", "serial", "ble", "ssh"}
ALLOWED_TRANSFORMS = {"localize", "tutor", "hardware_explainer"}


class TerminalOrchestrationService:
    def __init__(self, core):
        self.core = core
        self._lock = RLock()
        self._targets = {}

    def get_stats(self):
        with self._lock:
            return {
                "open_targets": len(self._targets),
                "targets": [self._target_view(record) for record in self._targets.values()],
            }

    def list_targets(self):
        with self._lock:
            return [self._target_view(record) for record in self._targets.values()]

    def open_target(self, descriptor, trace_context=None):
        normalized = self._validate_descriptor(descriptor)
        trace_kwargs = _trace_kwargs(trace_context)
        opened_payload = {
            "target_id": normalized["target_id"],
            "target_type": normalized["target_type"],
            "transport": normalized["transport"],
        }
        with self._lock:
            existing = self._targets.get(normalized["target_id"])
            if existing:
                existing.setdefault("buffer_version", 0)
                existing.setdefault("derived_cache", {})
                existing.setdefault("last_good_derived", {})
                existing.setdefault("derived_output_lines", {})
                existing_descriptor = dict(existing.get("descriptor") or {})
                # Idempotent open: keep history/transforms for existing target IDs.
                existing_descriptor.update(
                    {
                        "target_type": normalized["target_type"],
                        "session_id": normalized["session_id"],
                        "agent_id": normalized.get("agent_id"),
                        "transport": dict(normalized["transport"]),
                        "meta": dict(normalized["meta"]),
                    }
                )
                existing["descriptor"] = existing_descriptor
                opened_payload["reused"] = True
                target_view = self._target_view(existing)
            else:
                self._targets[normalized["target_id"]] = {
                    "descriptor": normalized,
                    "transforms": [],
                    "recent_output_lines": [],
                    "buffer_version": 0,
                    "derived_cache": {},
                    "last_good_derived": {},
                    "derived_output_lines": {},
                }
                opened_payload["reused"] = False
                target_view = self._target_view(
                    {
                        "descriptor": normalized,
                        "transforms": [],
                        "recent_output_lines": [],
                        "buffer_version": 0,
                        "derived_cache": {},
                        "last_good_derived": {},
                        "derived_output_lines": {},
                    }
                )
        emit_trace(
            build_trace_event(
                event_type="terminal_session_opened",
                run_id=f"terminal:{normalized['target_id']}",
                outcome="success",
                payload=opened_payload,
                **trace_kwargs,
            )
        )
        return target_view

    def close_target(self, target_id):
        with self._lock:
            removed = self._targets.pop(target_id, None)
        if removed is None:
            return False
        emit_trace(
            build_trace_event(
                event_type="terminal_session_closed",
                run_id=f"terminal:{target_id}",
                outcome="success",
                payload={
                    "target_id": target_id,
                    "target_type": removed["descriptor"]["target_type"],
                },
            )
        )
        return True

    def clear_output(self, target_id, trace_context=None):
        with self._lock:
            record = self._targets.get(target_id)
            if not record:
                return False
            record["recent_output_lines"] = []
            next_version = int(record.get("buffer_version") or 0) + 1
            record["buffer_version"] = next_version
            record["derived_cache"] = _prune_derived_cache(record.get("derived_cache"), keep_buffer_version=next_version)
            record["derived_output_lines"] = {}
        emit_trace(
            build_trace_event(
                event_type="terminal_output",
                run_id=f"terminal:{target_id}:clear:{uuid.uuid4().hex[:8]}",
                outcome="success",
                payload={
                    "target_id": target_id,
                    "target_type": record["descriptor"]["target_type"],
                    "derived": False,
                    "cleared": True,
                    "source_buffer_version": next_version,
                    "output": "",
                },
                **_trace_kwargs(trace_context),
            )
        )
        return True

    def set_transform(self, target_id, module, config=None, trace_context=None):
        module = str(module or "").strip()
        if module not in ALLOWED_TRANSFORMS:
            raise ValueError(f"unsupported_transform_module:{module}")
        run_id = f"terminal:{target_id}:transform:{uuid.uuid4().hex[:8]}"
        cfg = dict(config or {})
        target_type = None
        snapshot_output = ""
        source_buffer_version = 0
        cached_entry = None
        last_good_entry = None
        with self._lock:
            record = self._targets.get(target_id)
            if not record:
                raise KeyError("target_not_found")
            transforms = [item for item in record["transforms"] if item.get("module") != module]
            transforms.append({"module": module, "config": cfg})
            record["transforms"] = transforms
            # Config changes (for example, localize language) should not append across prior overlay language/state.
            lines_map = dict(record.get("derived_output_lines") or {})
            lines_map[module] = []
            record["derived_output_lines"] = lines_map
            target_type = record["descriptor"]["target_type"]
            source_buffer_version = int(record.get("buffer_version") or 0)
            snapshot_output = "\n".join(list(record.get("recent_output_lines") or []))
            cache_variant = _transform_cache_variant(module=module, config=cfg)
            cache_key = _derived_cache_key(
                module=module,
                source_buffer_version=source_buffer_version,
                cache_variant=cache_variant,
            )
            cached_entry = dict((record.get("derived_cache") or {}).get(cache_key) or {})
            last_good_key = _last_good_key(module=module, config=cfg)
            last_good_map = dict(record.get("last_good_derived") or {})
            last_good_entry = dict(last_good_map.get(last_good_key) or {})

        derived_trace_id = None
        quality_status = "skipped"
        model_meta = {}
        transform_latency_ms = 0
        cached = bool(cached_entry)
        if snapshot_output.strip():
            if cached_entry:
                rendered = str(cached_entry.get("output") or "")
                model_meta = dict(cached_entry.get("model") or {})
                quality_status = str(cached_entry.get("quality_status") or "cached")
                transform_latency_ms = int(cached_entry.get("latency_ms") or 0)
                derived_trace_id = str(cached_entry.get("derived_trace_id") or "") or None
                with self._lock:
                    current = self._targets.get(target_id)
                    if current is not None:
                        lines_map = dict(current.get("derived_output_lines") or {})
                        lines_map[module] = _cap_lines(str(rendered or "").splitlines(), limit=120)
                        current["derived_output_lines"] = lines_map
            else:
                rendered, model_meta, quality_status, transform_latency_ms = _apply_transform(
                    module=module,
                    raw_output=snapshot_output,
                    command="(transform_apply)",
                    exit_code=0,
                    config=cfg,
                )
                rendered, model_meta, quality_status, transform_latency_ms = _reuse_last_good_if_needed(
                    module=module,
                    rendered=rendered,
                    model_meta=model_meta,
                    quality_status=quality_status,
                    transform_latency_ms=transform_latency_ms,
                    last_good_entry=last_good_entry,
                )
                source_line_count = _line_count(snapshot_output)
                derived_line_count = _line_count(rendered)
                derived_event = emit_trace(
                    build_trace_event(
                        event_type="terminal_output",
                        run_id=run_id,
                        outcome="success",
                        payload={
                            "target_id": target_id,
                            "target_type": target_type,
                            "derived": True,
                            "module": module,
                            "language": str(cfg.get("language") or "").strip().lower() if module == "localize" else None,
                            "source_buffer_version": source_buffer_version,
                            "quality_status": quality_status,
                            "source_line_count": source_line_count,
                            "derived_line_count": derived_line_count,
                            "output": rendered,
                        },
                        model=model_meta,
                        timing={"latency_ms": int(transform_latency_ms)},
                        **_trace_kwargs(trace_context),
                    )
                )
                derived_trace_id = derived_event["trace_id"]
                cache_entry = {
                    "module": module,
                    "source_buffer_version": source_buffer_version,
                    "output": rendered,
                    "language": str(cfg.get("language") or "").strip().lower() if module == "localize" else None,
                    "quality_status": quality_status,
                    "model": dict(model_meta or {}),
                    "latency_ms": int(transform_latency_ms),
                    "derived_trace_id": derived_trace_id,
                    "emitted_at": derived_event.get("timestamp"),
                }
                with self._lock:
                    current = self._targets.get(target_id)
                    if current is not None:
                        current_cache = dict(current.get("derived_cache") or {})
                        current_cache[
                            _derived_cache_key(
                                module=module,
                                source_buffer_version=source_buffer_version,
                                cache_variant=cache_variant,
                            )
                        ] = cache_entry
                        current["derived_cache"] = _prune_derived_cache(
                            current_cache,
                            keep_buffer_version=source_buffer_version,
                        )
                        lines_map = dict(current.get("derived_output_lines") or {})
                        lines_map[module] = _cap_lines(str(rendered or "").splitlines(), limit=120)
                        current["derived_output_lines"] = lines_map
                        if quality_status == "ok":
                            last_good = dict(current.get("last_good_derived") or {})
                            last_good[last_good_key] = dict(cache_entry)
                            current["last_good_derived"] = last_good
        emit_trace(
            build_trace_event(
                event_type="terminal_transform_applied",
                run_id=run_id,
                outcome="success",
                payload={
                    "target_id": target_id,
                    "module": module,
                    "config": cfg,
                    "source_buffer_version": source_buffer_version,
                    "quality_status": quality_status,
                    "derived_trace_id": derived_trace_id,
                    "cached": cached,
                },
                model=model_meta,
                timing={"latency_ms": int(transform_latency_ms)},
                **_trace_kwargs(trace_context),
            )
        )
        # Emit an immediate derived overlay event from current terminal history
        # so UI can render overlay without requiring a follow-up command execution.
        with self._lock:
            record = self._targets.get(target_id)
            if record is None:
                raise KeyError("target_not_found")
            return self._target_view(record)

    def run_command(self, target_id, command, cwd=None, env=None, timeout_sec=30, trace_context=None):
        run_id = f"terminal:{target_id}:{uuid.uuid4().hex[:8]}"
        payload_base = {"target_id": target_id, "command": command}
        trace_kwargs = _trace_kwargs(trace_context)
        emit_trace(
            build_trace_event(
                event_type="terminal_command_requested",
                run_id=run_id,
                outcome="success",
                payload=payload_base,
                **trace_kwargs,
            )
        )

        with self._lock:
            record = self._targets.get(target_id)
            if not record:
                emit_trace(
                    build_trace_event(
                        event_type="terminal_command_failed",
                        run_id=run_id,
                        outcome="failed",
                        payload={**payload_base, "reason": "target_not_found"},
                        **trace_kwargs,
                    )
                )
                raise KeyError("target_not_found")
            descriptor = dict(record["descriptor"])
            transforms = list(record["transforms"])

        emit_trace(
            build_trace_event(
                event_type="terminal_command_authorized",
                run_id=run_id,
                outcome="success",
                payload={**payload_base, "target_type": descriptor["target_type"]},
                **trace_kwargs,
            )
        )

        try:
            proc_result = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                text=True,
                timeout=max(1, int(timeout_sec)),
                cwd=cwd or None,
                env=self._build_env(env),
            )
        except subprocess.TimeoutExpired as exc:
            emit_trace(
                build_trace_event(
                    event_type="terminal_command_failed",
                    run_id=run_id,
                    outcome="failed",
                    payload={**payload_base, "reason": "timeout", "timeout_sec": int(timeout_sec)},
                    **trace_kwargs,
                )
            )
            raise TimeoutError(f"command_timeout:{int(timeout_sec)}s") from exc

        raw_output = (proc_result.stdout or "") + (proc_result.stderr or "")
        recent_lines = _recent_lines(raw_output, limit=24)
        command_header = f"$ {command}"
        delta_lines = [command_header] + recent_lines
        delta_text = "\n".join(delta_lines)
        current_buffer_version = 0
        with self._lock:
            target_record = self._targets.get(target_id)
            if target_record is not None:
                prior = list(target_record.get("recent_output_lines") or [])
                next_lines = _cap_lines(prior + [command_header] + recent_lines, limit=120)
                target_record["recent_output_lines"] = next_lines
                current_buffer_version = int(target_record.get("buffer_version") or 0) + 1
                target_record["buffer_version"] = current_buffer_version
                target_record["derived_cache"] = _prune_derived_cache(
                    target_record.get("derived_cache"),
                    keep_buffer_version=current_buffer_version,
                )

        source_event = emit_trace(
            build_trace_event(
                event_type="terminal_output",
                run_id=run_id,
                outcome="success",
                payload={
                    "target_id": target_id,
                    "target_type": descriptor["target_type"],
                    "derived": False,
                    "source_buffer_version": current_buffer_version,
                    "exit_code": int(proc_result.returncode),
                    "output": raw_output,
                },
                **trace_kwargs,
            )
        )

        transformed_outputs = []
        for transform in transforms:
            module = transform.get("module")
            config = dict(transform.get("config") or {})
            cache_variant = _transform_cache_variant(module=module, config=config)
            cache_key = _derived_cache_key(
                module=module,
                source_buffer_version=current_buffer_version,
                cache_variant=cache_variant,
            )
            last_good_entry = {}
            prior_derived_lines = []
            transform_latency_ms = 0
            with self._lock:
                live_record = self._targets.get(target_id)
                cached_entry = dict((live_record.get("derived_cache") or {}).get(cache_key) or {}) if live_record else {}
                last_good_key = _last_good_key(module=module, config=config)
                last_good_entry = dict((live_record.get("last_good_derived") or {}).get(last_good_key) or {}) if live_record else {}
                prior_derived_lines = list((live_record.get("derived_output_lines") or {}).get(module) or []) if live_record else []
            if cached_entry:
                rendered = str(cached_entry.get("output") or "")
                model_meta = dict(cached_entry.get("model") or {})
                quality_status = str(cached_entry.get("quality_status") or "cached")
                transform_latency_ms = int(cached_entry.get("latency_ms") or 0)
                derived_trace_id = str(cached_entry.get("derived_trace_id") or "") or None
                derived_event = {"trace_id": derived_trace_id}
                with self._lock:
                    current = self._targets.get(target_id)
                    if current is not None:
                        lines_map = dict(current.get("derived_output_lines") or {})
                        lines_map[module] = _cap_lines(str(rendered or "").splitlines(), limit=120)
                        current["derived_output_lines"] = lines_map
            else:
                rendered_delta, model_meta, quality_status, transform_latency_ms = _apply_transform(
                    module=module,
                    raw_output=delta_text,
                    command=command,
                    exit_code=int(proc_result.returncode),
                    config=config,
                )
                rendered_delta, model_meta, quality_status, transform_latency_ms = _reuse_last_good_if_needed(
                    module=module,
                    rendered=rendered_delta,
                    model_meta=model_meta,
                    quality_status=quality_status,
                    transform_latency_ms=transform_latency_ms,
                    last_good_entry=last_good_entry,
                )
                rendered_lines = _merge_derived_lines(
                    prior_lines=prior_derived_lines,
                    delta_lines=str(rendered_delta or "").splitlines(),
                    limit=120,
                )
                rendered = "\n".join(rendered_lines)
                source_line_count = _line_count(delta_text)
                derived_line_count = _line_count(rendered_delta)
                derived_event = emit_trace(
                    build_trace_event(
                        event_type="terminal_output",
                        run_id=run_id,
                        outcome="success",
                        payload={
                            "target_id": target_id,
                            "target_type": descriptor["target_type"],
                            "derived": True,
                            "module": module,
                            "language": str(config.get("language") or "").strip().lower() if module == "localize" else None,
                            "source_buffer_version": current_buffer_version,
                            "quality_status": quality_status,
                            "source_line_count": source_line_count,
                            "derived_line_count": derived_line_count,
                            "derived_buffer_line_count": len(rendered_lines),
                            "source_trace_id": source_event["trace_id"],
                            "output": rendered,
                        },
                        model=model_meta,
                        timing={"latency_ms": int(transform_latency_ms)},
                        **trace_kwargs,
                    )
                )
                with self._lock:
                    current = self._targets.get(target_id)
                    if current is not None:
                        current_cache = dict(current.get("derived_cache") or {})
                        current_cache[cache_key] = {
                            "module": module,
                            "source_buffer_version": current_buffer_version,
                            "output": rendered,
                            "language": str(config.get("language") or "").strip().lower() if module == "localize" else None,
                            "quality_status": quality_status,
                            "model": dict(model_meta or {}),
                            "latency_ms": int(transform_latency_ms),
                            "derived_trace_id": derived_event.get("trace_id"),
                            "emitted_at": derived_event.get("timestamp"),
                        }
                        current["derived_cache"] = _prune_derived_cache(
                            current_cache,
                            keep_buffer_version=current_buffer_version,
                        )
                        lines_map = dict(current.get("derived_output_lines") or {})
                        lines_map[module] = list(rendered_lines)
                        current["derived_output_lines"] = lines_map
                        if quality_status in {"ok", "reused_last_good"}:
                            last_good = dict(current.get("last_good_derived") or {})
                            last_good[last_good_key] = dict(current_cache[cache_key])
                            current["last_good_derived"] = last_good
            emit_trace(
                build_trace_event(
                    event_type="terminal_transform_applied",
                    run_id=run_id,
                    outcome="success",
                    payload={
                        "target_id": target_id,
                        "module": module,
                        "source_buffer_version": current_buffer_version,
                        "quality_status": quality_status,
                        "source_trace_id": source_event["trace_id"],
                        "derived_trace_id": derived_event.get("trace_id"),
                        "cached": bool(cached_entry),
                    },
                    model=model_meta,
                    timing={"latency_ms": int(transform_latency_ms)},
                    **trace_kwargs,
                )
            )
            transformed_outputs.append({"module": module, "output": rendered})

        if proc_result.returncode == 0:
            emit_trace(
                build_trace_event(
                    event_type="terminal_command_applied",
                    run_id=run_id,
                    outcome="success",
                    payload={**payload_base, "exit_code": int(proc_result.returncode)},
                    **trace_kwargs,
                )
            )
        else:
            emit_trace(
                build_trace_event(
                    event_type="terminal_command_failed",
                    run_id=run_id,
                    outcome="failed",
                    payload={
                        **payload_base,
                        "exit_code": int(proc_result.returncode),
                        "reason": "non_zero_exit",
                    },
                    **trace_kwargs,
                )
            )

        return {
            "target_id": target_id,
            "run_id": run_id,
            "exit_code": int(proc_result.returncode),
            "stdout": proc_result.stdout or "",
            "stderr": proc_result.stderr or "",
            "raw_output": raw_output,
            "transformed_outputs": transformed_outputs,
        }

    def _validate_descriptor(self, descriptor):
        if not isinstance(descriptor, dict):
            raise ValueError("target_descriptor_required")
        target_type = str(descriptor.get("target_type") or "").strip()
        if target_type not in ALLOWED_TARGET_TYPES:
            raise ValueError(f"invalid_target_type:{target_type}")
        transport = descriptor.get("transport") or {}
        transport_kind = str(transport.get("kind") or "").strip()
        if transport_kind not in ALLOWED_TRANSPORT_KINDS:
            raise ValueError(f"invalid_transport_kind:{transport_kind}")
        session_id = str(descriptor.get("session_id") or self.core.session_id or "").strip()
        if not session_id:
            raise ValueError("session_id_required")
        target_id = str(descriptor.get("target_id") or "").strip() or f"terminal-{uuid.uuid4().hex[:12]}"
        meta = dict(descriptor.get("meta") or {})
        return {
            "target_id": target_id,
            "target_type": target_type,
            "session_id": session_id,
            "agent_id": descriptor.get("agent_id"),
            "transport": {
                "kind": transport_kind,
                "ref": str(transport.get("ref") or ""),
            },
            "meta": {
                "label": str(meta.get("label") or target_id),
                "encoding": str(meta.get("encoding") or "utf-8"),
                "cols": int(meta.get("cols") or 120),
                "rows": int(meta.get("rows") or 40),
            },
        }

    def _target_view(self, record):
        descriptor = dict(record["descriptor"])
        buffer_version = int(record.get("buffer_version") or 0)
        derived_views = _derived_views_for_buffer(record.get("derived_cache"), buffer_version)
        return {
            "target_id": descriptor["target_id"],
            "target_type": descriptor["target_type"],
            "session_id": descriptor["session_id"],
            "agent_id": descriptor.get("agent_id"),
            "transport": dict(descriptor["transport"]),
            "meta": dict(descriptor["meta"]),
            "transforms": [dict(item) for item in record.get("transforms", [])],
            "recent_output_lines": list(record.get("recent_output_lines") or []),
            "buffer_version": buffer_version,
            "derived_views": derived_views,
        }

    def _build_env(self, env):
        merged = dict(os.environ)
        if isinstance(env, dict):
            for key, value in env.items():
                merged[str(key)] = str(value)
        return merged


def _apply_transform(module, raw_output, command, exit_code, config):
    fallback_localized = None
    if module == "localize":
        language = str((config or {}).get("language") or "italian").strip().lower()
        fallback_localized = _line_preserving_localize_fallback(raw_output, language)
    model_meta = _transform_model_meta(module)
    if str(AI_PROVIDER or "").lower() == "ollama" and module in {"localize", "tutor", "hardware_explainer"}:
        model_text, latency_ms = _run_ollama_terminal_transform(
            module=module,
            raw_output=raw_output,
            command=command,
            exit_code=exit_code,
            config=config or {},
        )
        if model_text:
            if module == "localize" and _is_collapsed_localization(model_text, raw_output):
                return fallback_localized, {"provider": "fallback", "name": "line_preserving_localize"}, "fallback_collapsed", int(latency_ms)
            if module == "localize" and _is_noop_localization(model_text, raw_output):
                return fallback_localized, {"provider": "fallback", "name": "line_preserving_localize"}, "fallback_noop", int(latency_ms)
            return model_text, model_meta, "ok", int(latency_ms)

    if module == "localize":
        return fallback_localized, {"provider": "fallback", "name": "line_preserving_localize"}, "fallback_provider", 0
    if module == "tutor":
        return (
            f"{raw_output}\n"
            f"[Tutor] Command `{command}` finished with exit code {exit_code}. "
            "Review stdout/stderr and retry with narrower scope on errors."
        ), {"provider": "fallback", "name": "deterministic_tutor"}, "fallback_provider", 0
    if module == "hardware_explainer":
        return (
            f"{raw_output}\n"
            "[Hardware Explainer] Interpreting output for physical terminal operators: "
            "check connectivity, power state, and transport stability before rerun."
        ), {"provider": "fallback", "name": "deterministic_hardware_explainer"}, "fallback_provider", 0
    return raw_output, {}, "ok", 0


def _recent_lines(text, limit=24):
    lines = [line for line in str(text or "").splitlines() if line.strip()]
    if not lines:
        return ["(no output)"]
    return lines[-max(1, int(limit)):]


def _cap_lines(lines, limit=120):
    items = list(lines or [])
    return items[-max(1, int(limit)):]


def _line_count(text):
    return len(str(text or "").splitlines())


def _is_collapsed_localization(localized_text, raw_output):
    raw_lines = [line for line in str(raw_output or "").splitlines() if line.strip()]
    localized_lines = [line for line in str(localized_text or "").splitlines() if line.strip()]
    if len(raw_lines) <= 1:
        return False
    if not localized_lines:
        return True
    return len(localized_lines) < max(2, int(len(raw_lines) * 0.6))


def _is_noop_localization(localized_text, raw_output):
    raw_lines = [str(line).strip().lower() for line in str(raw_output or "").splitlines() if str(line).strip()]
    localized_lines = [str(line).strip().lower() for line in str(localized_text or "").splitlines() if str(line).strip()]
    if not raw_lines or not localized_lines:
        return False
    if len(raw_lines) != len(localized_lines):
        return False
    matches = 0
    for idx, raw_line in enumerate(raw_lines):
        if raw_line == localized_lines[idx]:
            matches += 1
    ratio = float(matches) / float(max(1, len(raw_lines)))
    return ratio >= 0.85


def _line_preserving_localize_fallback(raw_output, language):
    lines = str(raw_output or "").splitlines()
    if not lines:
        return f"[localized:{language}]"
    tagged = [f"[{language}] {line}" if line else f"[{language}]" for line in lines]
    return "\n".join(tagged)


def _run_ollama_terminal_transform(module, raw_output, command, exit_code, config):
    model = get_ai_route_model(module, fallback_model=AI_OLLAMA_MODEL)
    timeout_sec = max(5, int(AI_OLLAMA_TIMEOUT_SEC or 40))
    safe_output = str(raw_output or "")[:4000]

    if module == "localize":
        language = str((config or {}).get("language") or "italian").strip() or "italian"
        instructions = (
            f"Translate terminal output to {language}.\n"
            "Rules:\n"
            "- Preserve line order and command semantics.\n"
            "- Do not summarize.\n"
            "- Return only translated terminal text (plain text, no markdown).\n"
        )
        prompt = (
            f"{instructions}\n\n"
            f"Command: {command}\n"
            f"Exit code: {exit_code}\n"
            f"Terminal output:\n{safe_output}"
        )
        return _run_ollama_prompt(model, prompt, timeout_sec)
    elif module == "tutor":
        instructions = (
            "Explain terminal output for an operator in concise plain language. "
            "Include: what happened, why it likely happened, and one next step. "
            "Return plain text only, no markdown."
        )
    else:
        instructions = (
            "Interpret terminal output for a hardware operator. "
            "Summarize likely device/system implications and one immediate check. "
            "Return plain text only, no markdown."
        )

    prompt = (
        f"{instructions}\n\n"
        f"Command: {command}\n"
        f"Exit code: {exit_code}\n"
        f"Terminal output:\n{safe_output}"
    )
    return _run_ollama_prompt(model, prompt, timeout_sec)


def _run_ollama_prompt(model, prompt, timeout_sec):
    start = time.perf_counter()
    try:
        result = subprocess.run(
            ["ollama", "run", model, prompt],
            capture_output=True,
            text=True,
            encoding="utf-8",
            timeout=timeout_sec,
        )
        elapsed_ms = int((time.perf_counter() - start) * 1000.0)
        return (result.stdout or "").strip(), max(0, elapsed_ms)
    except Exception:
        elapsed_ms = int((time.perf_counter() - start) * 1000.0)
        return "", max(0, elapsed_ms)


def _transform_model_meta(module):
    provider = str(AI_PROVIDER or "").strip().lower()
    configured_model = get_ai_route_model(module, fallback_model=AI_OLLAMA_MODEL)
    if provider == "ollama" and module in {"localize", "tutor", "hardware_explainer"}:
        return {"provider": "ollama", "name": configured_model or "phi3"}
    if provider:
        return {"provider": provider, "name": configured_model or "fallback"}
    return {"provider": "fallback", "name": "deterministic"}


def _reuse_last_good_if_needed(module, rendered, model_meta, quality_status, transform_latency_ms, last_good_entry):
    if str(module or "").strip() != "localize":
        return rendered, model_meta, quality_status, transform_latency_ms
    status = str(quality_status or "").strip()
    if not status.startswith("fallback_"):
        return rendered, model_meta, quality_status, transform_latency_ms
    if not isinstance(last_good_entry, dict):
        return rendered, model_meta, quality_status, transform_latency_ms
    last_output = str(last_good_entry.get("output") or "")
    if not last_output.strip():
        return rendered, model_meta, quality_status, transform_latency_ms
    last_model = dict(last_good_entry.get("model") or {})
    return last_output, last_model, "reused_last_good", transform_latency_ms


def _derived_cache_key(module, source_buffer_version, cache_variant=""):
    return f"{str(module or '').strip()}::{int(source_buffer_version or 0)}::{str(cache_variant or '').strip()}"


def _transform_cache_variant(module, config):
    mod = str(module or "").strip().lower()
    cfg = dict(config or {})
    if mod == "localize":
        language = str(cfg.get("language") or "italian").strip().lower()
        return f"language={language}"
    # Stable generic variant for future transform configs.
    if not cfg:
        return ""
    try:
        return json.dumps(cfg, sort_keys=True, separators=(",", ":"))
    except Exception:
        return ""


def _last_good_key(module, config):
    return f"{str(module or '').strip().lower()}::{_transform_cache_variant(module, config)}"


def _prune_derived_cache(cache, keep_buffer_version):
    if not isinstance(cache, dict):
        return {}
    keep_version = int(keep_buffer_version or 0)
    pruned = {}
    for key, value in cache.items():
        if not isinstance(value, dict):
            continue
        version = int(value.get("source_buffer_version") or 0)
        if version == keep_version:
            pruned[str(key)] = dict(value)
    return pruned


def _derived_views_for_buffer(cache, source_buffer_version):
    if not isinstance(cache, dict):
        return []
    buffer_version = int(source_buffer_version or 0)
    views = []
    for value in cache.values():
        if not isinstance(value, dict):
            continue
        version = int(value.get("source_buffer_version") or 0)
        if version != buffer_version:
            continue
        views.append(
            {
                "module": str(value.get("module") or ""),
                "source_buffer_version": version,
                "output": str(value.get("output") or ""),
                "language": value.get("language"),
                "quality_status": str(value.get("quality_status") or ""),
                "model": dict(value.get("model") or {}),
                "derived_trace_id": value.get("derived_trace_id"),
                "emitted_at": value.get("emitted_at"),
            }
        )
    views.sort(key=lambda item: str(item.get("emitted_at") or ""))
    return views


def _merge_derived_lines(prior_lines, delta_lines, limit=120):
    prior = list(prior_lines or [])
    delta = list(delta_lines or [])
    if not prior:
        return _cap_lines(delta, limit=limit)
    if not delta:
        return _cap_lines(prior, limit=limit)

    # If model returns the full derived buffer (or a large overlapping prefix), avoid duplicate append.
    max_overlap = min(len(prior), len(delta))
    overlap = 0
    for size in range(max_overlap, 0, -1):
        if prior[-size:] == delta[:size]:
            overlap = size
            break
    merged = prior + delta[overlap:]
    return _cap_lines(merged, limit=limit)


def _trace_kwargs(trace_context):
    context = dict(trace_context or {})
    return {
        "agent_id": context.get("agent_id"),
        "task_id": context.get("task_id"),
        "chunk_id": context.get("chunk_id"),
        "zone_id": context.get("zone_id"),
    }
