"""OTLP HTTP JSON normalization for otelctx."""

from __future__ import annotations

from collections import defaultdict
from datetime import UTC, datetime

from otelctx.db.repository import LogRecord, MetricRecord, SpanRecord, TraceRecord

STATUS_MAP = {
    0: "UNSET",
    1: "OK",
    2: "ERROR",
}

KIND_MAP = {
    0: "INTERNAL",
    1: "SERVER",
    2: "CLIENT",
    3: "PRODUCER",
    4: "CONSUMER",
}


def normalize_otlp_http_json(payload: dict[str, object]) -> list[TraceRecord]:
    resource_spans = payload.get("resourceSpans")
    if not isinstance(resource_spans, list):
        return []

    grouped_spans: dict[str, list[SpanRecord]] = defaultdict(list)
    trace_roots: dict[str, dict[str, str]] = {}

    for resource_span in resource_spans:
        if not isinstance(resource_span, dict):
            continue
        resource = resource_span.get("resource") if isinstance(resource_span.get("resource"), dict) else {}
        service_name = _resource_service_name(resource)

        scopes = resource_span.get("scopeSpans")
        if not isinstance(scopes, list):
            scopes = resource_span.get("instrumentationLibrarySpans") if isinstance(resource_span.get("instrumentationLibrarySpans"), list) else []

        for scope_span in scopes:
            if not isinstance(scope_span, dict):
                continue
            spans = scope_span.get("spans") if isinstance(scope_span.get("spans"), list) else []
            for span in spans:
                if not isinstance(span, dict):
                    continue
                trace_id = str(span.get("traceId") or "").strip()
                span_id = str(span.get("spanId") or "").strip()
                if not trace_id or not span_id:
                    continue

                status_payload = span.get("status") if isinstance(span.get("status"), dict) else {}
                status_code = _status_code(status_payload.get("code"))
                kind = _span_kind(span.get("kind"))
                attributes = _attributes_to_map(span.get("attributes"))

                span_record = SpanRecord(
                    span_id=span_id,
                    parent_span_id=str(span.get("parentSpanId") or "").strip(),
                    name=str(span.get("name") or "").strip() or "unnamed-span",
                    service_name=service_name,
                    kind=kind,
                    status_code=status_code,
                    start_time=_nanos_to_iso(span.get("startTimeUnixNano")),
                    end_time=_nanos_to_iso(span.get("endTimeUnixNano")),
                    attributes=attributes,
                )
                grouped_spans[trace_id].append(span_record)

                root = trace_roots.setdefault(
                    trace_id,
                    {
                        "root_service_name": service_name,
                        "root_span_name": span_record.name,
                        "status_code": status_code,
                    },
                )
                if status_code == "ERROR":
                    root["status_code"] = "ERROR"

    return [
        TraceRecord(
            trace_id=trace_id,
            root_service_name=trace_roots[trace_id]["root_service_name"],
            root_span_name=trace_roots[trace_id]["root_span_name"],
            status_code=trace_roots[trace_id]["status_code"],
            spans=spans,
        )
        for trace_id, spans in grouped_spans.items()
    ]


def normalize_otlp_http_logs_json(payload: dict[str, object]) -> list[LogRecord]:
    resource_logs = payload.get("resourceLogs")
    if not isinstance(resource_logs, list):
        return []

    logs: list[LogRecord] = []
    for resource_log in resource_logs:
        if not isinstance(resource_log, dict):
            continue
        resource = resource_log.get("resource") if isinstance(resource_log.get("resource"), dict) else {}
        service_name = _resource_service_name(resource)
        scope_logs = resource_log.get("scopeLogs")
        if not isinstance(scope_logs, list):
            scope_logs = resource_log.get("instrumentationLibraryLogs") if isinstance(resource_log.get("instrumentationLibraryLogs"), list) else []
        for scope_log in scope_logs:
            if not isinstance(scope_log, dict):
                continue
            entries = scope_log.get("logRecords") if isinstance(scope_log.get("logRecords"), list) else []
            for entry in entries:
                if not isinstance(entry, dict):
                    continue
                severity_number = entry.get("severityNumber")
                logs.append(
                    LogRecord(
                        timestamp=_nanos_to_iso(entry.get("timeUnixNano")),
                        observed_time=_nanos_to_iso(entry.get("observedTimeUnixNano")),
                        service_name=service_name,
                        severity_text=str(entry.get("severityText") or _severity_text(severity_number)),
                        body=_any_value_to_python(entry.get("body")),
                        trace_id=str(entry.get("traceId") or "").strip(),
                        span_id=str(entry.get("spanId") or "").strip(),
                        attributes=_attributes_to_map(entry.get("attributes")),
                    )
                )
    return logs


def normalize_otlp_http_metrics_json(payload: dict[str, object]) -> list[MetricRecord]:
    resource_metrics = payload.get("resourceMetrics")
    if not isinstance(resource_metrics, list):
        return []

    metrics: list[MetricRecord] = []
    for resource_metric in resource_metrics:
        if not isinstance(resource_metric, dict):
            continue
        resource = resource_metric.get("resource") if isinstance(resource_metric.get("resource"), dict) else {}
        service_name = _resource_service_name(resource)
        scope_metrics = resource_metric.get("scopeMetrics")
        if not isinstance(scope_metrics, list):
            scope_metrics = resource_metric.get("instrumentationLibraryMetrics") if isinstance(resource_metric.get("instrumentationLibraryMetrics"), list) else []
        for scope_metric in scope_metrics:
            if not isinstance(scope_metric, dict):
                continue
            metric_entries = scope_metric.get("metrics") if isinstance(scope_metric.get("metrics"), list) else []
            for metric in metric_entries:
                if not isinstance(metric, dict):
                    continue
                metrics.extend(_metric_records_from_otlp_metric(service_name, metric))
    return metrics


def _resource_service_name(resource: dict[str, object]) -> str:
    attributes = resource.get("attributes")
    if not isinstance(attributes, list):
        return "unknown"
    for attribute in attributes:
        if not isinstance(attribute, dict):
            continue
        if attribute.get("key") != "service.name":
            continue
        value = attribute.get("value") if isinstance(attribute.get("value"), dict) else {}
        candidate = value.get("stringValue")
        if candidate:
            return str(candidate)
    return "unknown"


def _attributes_to_map(attributes: object) -> dict[str, object]:
    if not isinstance(attributes, list):
        return {}
    result: dict[str, object] = {}
    for attribute in attributes:
        if not isinstance(attribute, dict):
            continue
        key = attribute.get("key")
        value = attribute.get("value")
        if not isinstance(key, str) or not isinstance(value, dict):
            continue
        if "stringValue" in value:
            result[key] = value["stringValue"]
        elif "intValue" in value:
            result[key] = value["intValue"]
        elif "doubleValue" in value:
            result[key] = value["doubleValue"]
        elif "boolValue" in value:
            result[key] = value["boolValue"]
    return result


def _any_value_to_python(value: object) -> str:
    if not isinstance(value, dict):
        return ""
    if "stringValue" in value:
        return str(value["stringValue"])
    if "intValue" in value:
        return str(value["intValue"])
    if "doubleValue" in value:
        return str(value["doubleValue"])
    if "boolValue" in value:
        return str(value["boolValue"])
    return ""


def _nanos_to_iso(raw: object) -> str:
    if raw in (None, ""):
        return ""
    try:
        value = int(raw)
    except Exception:
        return ""
    seconds = value / 1_000_000_000
    return datetime.fromtimestamp(seconds, tz=UTC).isoformat().replace("+00:00", "Z")


def _status_code(raw: object) -> str:
    try:
        return STATUS_MAP.get(int(raw), "UNSET")
    except Exception:
        return "UNSET"


def _span_kind(raw: object) -> str:
    try:
        return KIND_MAP.get(int(raw), "INTERNAL")
    except Exception:
        return "INTERNAL"


def _severity_text(raw: object) -> str:
    try:
        value = int(raw)
    except Exception:
        return "INFO"
    if value >= 17:
        return "ERROR"
    if value >= 13:
        return "WARN"
    if value >= 9:
        return "INFO"
    return "DEBUG"


def _metric_records_from_otlp_metric(service_name: str, metric: dict[str, object]) -> list[MetricRecord]:
    name = str(metric.get("name") or "").strip()
    unit = str(metric.get("unit") or "").strip()
    records: list[MetricRecord] = []
    metric_types = {
        "gauge": metric.get("gauge"),
        "sum": metric.get("sum"),
        "histogram": metric.get("histogram"),
    }
    for metric_type, payload in metric_types.items():
        if not isinstance(payload, dict):
            continue
        data_points = payload.get("dataPoints") if isinstance(payload.get("dataPoints"), list) else []
        temporality = _aggregation_temporality(payload.get("aggregationTemporality")) if metric_type in {"sum", "histogram"} else ""
        for point in data_points:
            if not isinstance(point, dict):
                continue
            value = _metric_point_value(metric_type, point)
            records.append(
                MetricRecord(
                    service_name=service_name,
                    name=name,
                    metric_type=metric_type,
                    unit=unit,
                    temporality=temporality,
                    start_time=_nanos_to_iso(point.get("startTimeUnixNano")),
                    time=_nanos_to_iso(point.get("timeUnixNano")),
                    value=value,
                    attributes=_attributes_to_map(point.get("attributes")),
                )
            )
    return records


def _metric_point_value(metric_type: str, point: dict[str, object]) -> float:
    if metric_type == "histogram":
        try:
            return float(point.get("sum") or 0.0)
        except Exception:
            return 0.0
    try:
        if "asDouble" in point:
            return float(point["asDouble"])
        if "asInt" in point:
            return float(point["asInt"])
    except Exception:
        return 0.0
    return 0.0


def _aggregation_temporality(raw: object) -> str:
    try:
        value = int(raw)
    except Exception:
        return ""
    if value == 1:
        return "DELTA"
    if value == 2:
        return "CUMULATIVE"
    return ""
