"""OTEL offset/polling/extraction helpers."""

import json
import logging
import re
import time
from pathlib import Path

from armorer import db as armorer_db

logger = logging.getLogger("ArmorerCore")


def _load_otel_offsets(handler) -> dict[str, int]:
    if not handler._otel_offsets_file.exists():
        return {}
    try:
        raw = json.loads(handler._otel_offsets_file.read_text())
        if isinstance(raw, dict):
            return {str(k): int(v) for k, v in raw.items()}
    except Exception:
        return {}
    return {}


def _save_otel_offsets(handler) -> None:
    try:
        handler._otel_offsets_file.write_text(json.dumps(handler._otel_offsets, sort_keys=True))
    except Exception as e:
        logger.debug("Failed to save OTEL offsets: %s", e)


def _extract_otel_text(row: dict) -> str:
    if not isinstance(row, dict):
        return ""
    candidates = [
        row.get("body"),
        row.get("message"),
        row.get("text"),
        ((row.get("attributes") or {}).get("message") if isinstance(row.get("attributes"), dict) else None),
        ((row.get("attributes") or {}).get("openclaw.message") if isinstance(row.get("attributes"), dict) else None),
        ((row.get("attributes") or {}).get("prompt") if isinstance(row.get("attributes"), dict) else None),
        ((row.get("attributes") or {}).get("response") if isinstance(row.get("attributes"), dict) else None),
    ]
    for v in candidates:
        if isinstance(v, str) and v.strip():
            return v.strip()
    return ""


def _otlp_any_value(value: object) -> object:
    if not isinstance(value, dict):
        return value
    for key in [
        "stringValue",
        "intValue",
        "doubleValue",
        "boolValue",
        "bytesValue",
    ]:
        if key in value:
            return value.get(key)
    if "arrayValue" in value and isinstance(value.get("arrayValue"), dict):
        arr = value["arrayValue"].get("values", [])
        if isinstance(arr, list):
            return [_otlp_any_value(v) for v in arr]
    if "kvlistValue" in value and isinstance(value.get("kvlistValue"), dict):
        out: dict[str, object] = {}
        for item in value["kvlistValue"].get("values", []) if isinstance(value["kvlistValue"].get("values"), list) else []:
            if not isinstance(item, dict):
                continue
            k = str(item.get("key") or "").strip()
            if not k:
                continue
            out[k] = _otlp_any_value(item.get("value"))
        return out
    return value


def _otlp_attrs(attrs: object) -> dict[str, object]:
    out: dict[str, object] = {}
    if not isinstance(attrs, list):
        return out
    for item in attrs:
        if not isinstance(item, dict):
            continue
        key = str(item.get("key") or "").strip()
        if not key:
            continue
        out[key] = _otlp_any_value(item.get("value"))
    return out


def _extract_otel_records(handler, row: dict) -> list[dict[str, object]]:
    records: list[dict[str, object]] = []
    if not isinstance(row, dict):
        return records

    direct_text = _extract_otel_text(row)
    if direct_text:
        attrs = row.get("attributes") if isinstance(row.get("attributes"), dict) else {}
        records.append({"text": direct_text, "attributes": attrs})

    # OTLP JSON logs payload shape.
    for rl in row.get("resourceLogs", []) if isinstance(row.get("resourceLogs"), list) else []:
        if not isinstance(rl, dict):
            continue
        base_attrs = _otlp_attrs((rl.get("resource") or {}).get("attributes") if isinstance(rl.get("resource"), dict) else [])
        scope_logs = rl.get("scopeLogs")
        if not isinstance(scope_logs, list):
            scope_logs = rl.get("instrumentationLibraryLogs", [])
        if not isinstance(scope_logs, list):
            continue
        for sl in scope_logs:
            if not isinstance(sl, dict):
                continue
            for rec in sl.get("logRecords", []) if isinstance(sl.get("logRecords"), list) else []:
                if not isinstance(rec, dict):
                    continue
                attrs = dict(base_attrs)
                attrs.update(_otlp_attrs(rec.get("attributes")))
                body = _otlp_any_value(rec.get("body"))
                txt = " ".join(str(body).split()).strip()
                if txt:
                    records.append({"text": txt, "attributes": attrs})

    # OTLP JSON traces payload shape.
    for rs in row.get("resourceSpans", []) if isinstance(row.get("resourceSpans"), list) else []:
        if not isinstance(rs, dict):
            continue
        base_attrs = _otlp_attrs((rs.get("resource") or {}).get("attributes") if isinstance(rs.get("resource"), dict) else [])
        scope_spans = rs.get("scopeSpans")
        if not isinstance(scope_spans, list):
            scope_spans = rs.get("instrumentationLibrarySpans", [])
        if not isinstance(scope_spans, list):
            continue
        for ss in scope_spans:
            if not isinstance(ss, dict):
                continue
            for span in ss.get("spans", []) if isinstance(ss.get("spans"), list) else []:
                if not isinstance(span, dict):
                    continue
                attrs = dict(base_attrs)
                attrs.update(_otlp_attrs(span.get("attributes")))
                span_name = str(span.get("name") or "").strip()
                status = span.get("status") if isinstance(span.get("status"), dict) else {}
                status_msg = str(status.get("message") or "").strip()
                text = " ".join(x for x in [span_name, status_msg] if x).strip()
                if text:
                    records.append({"text": text, "attributes": attrs})
    return records


def _extract_otel_app_name(handler, row: dict, file_path: Path) -> str:
    attrs = row.get("attributes") if isinstance(row.get("attributes"), dict) else {}
    identities = [
        str(attrs.get("armorer.app") or "").strip(),
        str(attrs.get("app") or "").strip(),
        str(attrs.get("agent") or "").strip(),
        str(attrs.get("service.name") or "").strip(),
        str(attrs.get("serviceName") or "").strip(),
        str(file_path.stem or "").strip(),
    ]
    normalized = []
    for x in identities:
        s = x.lower()
        s = re.sub(r"[^a-z0-9_.:-]+", "", s)
        if s:
            normalized.append(s)

    for meta in handler._list_apps_with_openclaw_fallback():
        candidates = {
            str(meta.name or "").strip().lower(),
            str(meta.agent_id or "").strip().lower(),
            Path(str(meta.install_path or "")).name.strip().lower(),
        }
        candidates = {re.sub(r"[^a-z0-9_.:-]+", "", c) for c in candidates if c}
        for ident in normalized:
            if ident in candidates or any(ident in c for c in candidates) or any(c in ident for c in candidates):
                return meta.name
    return ""


def _poll_otel_events(handler) -> None:
    processed = 0
    for event_file in handler.otel_events_dir.glob("*.jsonl"):
        key = str(event_file)
        pos = int(handler._otel_offsets.get(key, 0))
        try:
            with open(event_file, "r") as f:
                f.seek(pos)
                while processed < 250:
                    line = f.readline()
                    if not line:
                        break
                    pos = f.tell()
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        row = json.loads(line)
                    except Exception:
                        continue
                    records = _extract_otel_records(handler, row)
                    if not records:
                        continue
                    for rec in records:
                        text = str(rec.get("text") or "").strip()
                        if not text:
                            continue
                        app_name = _extract_otel_app_name(handler, {"attributes": rec.get("attributes", {})}, event_file)
                        if not app_name:
                            continue
                        attrs = rec.get("attributes", {}) if isinstance(rec.get("attributes"), dict) else {}
                        scan = handler.guard.inspect_input(text)
                        try:
                            armorer_db.insert_runtime_event(
                                handler.config_dir,
                                {
                                    "ts": time.time(),
                                    "agent_alias": app_name,
                                    "agent_id": app_name,
                                    "event_type": "otel",
                                    "subtype": "guard_signal" if scan.suspicious else "record",
                                    "severity": "warn" if scan.suspicious else "info",
                                    "path": str(event_file),
                                    "verdict": "suspicious" if scan.suspicious else "observed",
                                    "process_name": str(attrs.get("service.name") or attrs.get("serviceName") or ""),
                                    "raw": {
                                        "text": text[:400],
                                        "attributes": attrs,
                                        "confidence": scan.confidence,
                                        "reasons": scan.reasons,
                                    },
                                },
                            )
                        except Exception:
                            pass
                        handler.guard.append_history(
                            conversation_id=f"agent:{app_name}",
                            role="agent_runtime",
                            text=text,
                            inspection=scan,
                        )
                        if scan.suspicious:
                            handler._notify_agent_guard_alert(
                                app_name=app_name,
                                file_path=str(event_file),
                                snippet=text[:400],
                                reasons=scan.reasons,
                                confidence=scan.confidence,
                            )
                            if handler._auto_steer_enabled(source="otel"):
                                steer_result = handler._auto_steer_agent(
                                    app_name=app_name,
                                    trigger=f"otel:{event_file.name}",
                                    snippet=text,
                                )
                                handler._audit(
                                    "auto_steer_applied",
                                    source="otel",
                                    app_name=app_name,
                                    event_file=str(event_file),
                                    result=steer_result,
                                )
                        processed += 1
                        if processed >= 250:
                            break
            handler._otel_offsets[key] = pos
        except Exception as e:
            logger.debug("OTEL event polling failed for %s: %s", event_file, e)
    if processed:
        _save_otel_offsets(handler)
