"""LogFormatter: serialize LogRecord to JSON and parse JSON back to LogRecord."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import Any, Dict, Optional

from .log_record import LogRecord

# JSON schema field name mapping: Python attribute -> JSON key
_FIELD_TO_KEY = {
    "timestamp": "Timestamp",
    "observed_timestamp": "ObservedTimestamp",
    "severity_text": "SeverityText",
    "severity_number": "SeverityNumber",
    "body": "Body",
    "resource": "Resource",
    "instrumentation_scope": "InstrumentationScope",
    "attributes": "Attributes",
    "trace_id": "TraceId",
    "span_id": "SpanId",
    "trace_flags": "TraceFlags",
    "event_name": "EventName",
}

_KEY_TO_FIELD = {v: k for k, v in _FIELD_TO_KEY.items()}

# Required JSON keys per schema
_REQUIRED_KEYS = {"Timestamp", "SeverityText", "SeverityNumber", "Body", "Resource"}

_VALID_SEVERITY_TEXTS = {"TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"}


def _encode_value(value: Any) -> Any:
    """Encode a value for JSON serialization, handling AnyValue types.

    If a value cannot be serialized (e.g. circular reference),
    return "[unserializable]".
    """
    if value is None:
        return None
    if isinstance(value, (str, int, float, bool)):
        return value
    if isinstance(value, dict):
        return {k: _encode_value(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        return [_encode_value(item) for item in value]
    # Fallback for unserializable types
    try:
        json.dumps(value)
        return value
    except (TypeError, ValueError, OverflowError):
        return "[unserializable]"


def _format_timestamp(dt: datetime) -> str:
    """Format a datetime as ISO 8601 UTC string."""
    # Ensure UTC
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    else:
        dt = dt.astimezone(timezone.utc)
    return dt.isoformat().replace("+00:00", "Z")


def _parse_timestamp(value: str) -> datetime:
    """Parse an ISO 8601 timestamp string to a UTC datetime."""
    # Handle 'Z' suffix
    s = value.replace("Z", "+00:00")
    return datetime.fromisoformat(s).astimezone(timezone.utc)


class LogFormatter:
    """Serializes LogRecord to JSON and parses JSON back to LogRecord.

    Conforms to the Standard Log Format JSON schema.
    """

    @staticmethod
    def serialize(record: LogRecord) -> str:
        """Serialize a LogRecord to a JSON string.

        - Omits fields that are None/null.
        - Encodes timestamps as ISO 8601 UTC.
        - Handles AnyValue types in Body and Attributes.
        - Falls back to "[unserializable]" for values that can't be serialized.
        """
        data: Dict[str, Any] = {}

        # Timestamp (required by schema)
        if record.timestamp is not None:
            data["Timestamp"] = _format_timestamp(record.timestamp)

        # ObservedTimestamp (optional)
        if record.observed_timestamp is not None:
            data["ObservedTimestamp"] = _format_timestamp(record.observed_timestamp)

        # SeverityText (required)
        data["SeverityText"] = record.severity_text

        # SeverityNumber (required)
        data["SeverityNumber"] = record.severity_number

        # Body (required)
        data["Body"] = _encode_value(record.body)

        # Resource (required)
        data["Resource"] = _encode_value(record.resource)

        # InstrumentationScope (optional)
        if record.instrumentation_scope is not None:
            data["InstrumentationScope"] = _encode_value(record.instrumentation_scope)

        # Attributes (optional)
        if record.attributes is not None:
            data["Attributes"] = _encode_value(record.attributes)

        # Trace context — omit if absent (Req 1.4)
        if record.trace_id is not None:
            data["TraceId"] = record.trace_id
        if record.span_id is not None:
            data["SpanId"] = record.span_id
        if record.trace_flags is not None:
            data["TraceFlags"] = record.trace_flags

        # EventName (optional)
        if record.event_name is not None:
            data["EventName"] = record.event_name

        return json.dumps(data, separators=(",", ":"))

    @staticmethod
    def parse(json_str: str) -> LogRecord:
        """Parse a JSON string into a LogRecord.

        Raises ValueError with a descriptive message for invalid input.
        """
        # Parse JSON
        try:
            data = json.loads(json_str)
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON: {e}") from e

        if not isinstance(data, dict):
            raise ValueError("JSON root must be an object")

        # Validate required fields
        for key in _REQUIRED_KEYS:
            if key not in data:
                raise ValueError(f"Missing required field: {key}")

        # Validate no unknown fields
        allowed_keys = set(_KEY_TO_FIELD.keys())
        unknown = set(data.keys()) - allowed_keys
        if unknown:
            raise ValueError(f"Unknown fields: {', '.join(sorted(unknown))}")

        # Validate SeverityText
        severity_text = data["SeverityText"]
        if not isinstance(severity_text, str):
            raise ValueError(f"SeverityText must be a string, got {type(severity_text).__name__}")
        if severity_text not in _VALID_SEVERITY_TEXTS:
            raise ValueError(
                f"Invalid SeverityText: '{severity_text}'. "
                f"Must be one of: {', '.join(sorted(_VALID_SEVERITY_TEXTS))}"
            )

        # Validate SeverityNumber
        severity_number = data["SeverityNumber"]
        if not isinstance(severity_number, int) or isinstance(severity_number, bool):
            raise ValueError(f"SeverityNumber must be an integer, got {type(severity_number).__name__}")
        if not (0 <= severity_number <= 24):
            raise ValueError(f"SeverityNumber must be 0-24, got {severity_number}")

        # Validate Resource
        resource = data["Resource"]
        if not isinstance(resource, dict):
            raise ValueError(f"Resource must be an object, got {type(resource).__name__}")
        if "service.name" not in resource:
            raise ValueError("Resource must contain 'service.name'")

        # Parse timestamps
        timestamp: Optional[datetime] = None
        if "Timestamp" in data:
            try:
                timestamp = _parse_timestamp(data["Timestamp"])
            except (ValueError, TypeError) as e:
                raise ValueError(f"Invalid Timestamp format: {e}") from e

        observed_timestamp: Optional[datetime] = None
        if "ObservedTimestamp" in data:
            try:
                observed_timestamp = _parse_timestamp(data["ObservedTimestamp"])
            except (ValueError, TypeError) as e:
                raise ValueError(f"Invalid ObservedTimestamp format: {e}") from e

        # Validate trace context formats if present
        trace_id = data.get("TraceId")
        if trace_id is not None:
            if not isinstance(trace_id, str) or len(trace_id) != 32:
                raise ValueError(f"TraceId must be a 32-character hex string, got '{trace_id}'")

        span_id = data.get("SpanId")
        if span_id is not None:
            if not isinstance(span_id, str) or len(span_id) != 16:
                raise ValueError(f"SpanId must be a 16-character hex string, got '{span_id}'")

        trace_flags = data.get("TraceFlags")
        if trace_flags is not None:
            if not isinstance(trace_flags, str) or len(trace_flags) != 2:
                raise ValueError(f"TraceFlags must be a 2-character hex string, got '{trace_flags}'")

        # Validate InstrumentationScope
        instrumentation_scope = data.get("InstrumentationScope")
        if instrumentation_scope is not None and not isinstance(instrumentation_scope, dict):
            raise ValueError(
                f"InstrumentationScope must be an object, got {type(instrumentation_scope).__name__}"
            )

        # Validate Attributes
        attributes = data.get("Attributes")
        if attributes is not None and not isinstance(attributes, dict):
            raise ValueError(f"Attributes must be an object, got {type(attributes).__name__}")

        return LogRecord(
            timestamp=timestamp,
            observed_timestamp=observed_timestamp,
            severity_text=severity_text,
            severity_number=severity_number,
            body=data["Body"],
            resource=resource,
            instrumentation_scope=instrumentation_scope,
            attributes=attributes,
            trace_id=trace_id,
            span_id=span_id,
            trace_flags=trace_flags,
            event_name=data.get("EventName"),
        )
