"""Export/import monitor configuration as JSON. Pure, no GTK.

Export captures every editable (RW) feature's current value — a superset of
ddcutil's native dumpvcp color-only profile. Writes are atomic (temp file in the
same directory + os.replace). Import validates structure and value ranges before
anything is sent to setvcp.
"""

from __future__ import annotations

import json
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from .models import Access, Display, Feature, ValueType

SCHEMA_VERSION = 1


@dataclass
class ImportEntry:
    """One validated value to apply on import."""

    code: int
    value: int
    continuous: bool


@dataclass
class Profile:
    monitor_id: str
    mccs_version: str
    entries: list[ImportEntry]


def build_profile(display: Display, features: list[Feature], *, exported_at: str) -> dict[str, Any]:
    """Assemble a JSON-serializable profile from current feature values."""
    values: dict[str, Any] = {}
    for feature in features:
        if feature.access != Access.READ_WRITE or feature.value is None:
            continue
        v = feature.value
        if not v.supported or v.current is None:
            continue
        values[f"{feature.code:02X}"] = {
            "name": feature.name,
            "type": feature.vtype.value,
            "value": v.current,
        }
    return {
        "schema": SCHEMA_VERSION,
        "monitor": display.monitor_id,
        "mccs_version": display.vcp_version,
        "exported_at": exported_at,
        "values": values,
    }


def atomic_write_json(path: Path, obj: Any) -> None:
    """Write JSON to path atomically (temp file in same dir + os.replace)."""
    path = path.expanduser()
    fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=".big-ddcgui-", suffix=".json")
    tmp = Path(tmp_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            json.dump(obj, fh, indent=2, sort_keys=True)
            fh.write("\n")
            fh.flush()
            os.fsync(fh.fileno())
        os.replace(tmp, path)
    except BaseException:
        tmp.unlink(missing_ok=True)
        raise


def export_profile(path: Path, profile: dict[str, Any]) -> None:
    """Atomically write the profile JSON to path."""
    atomic_write_json(path, profile)


def load_profile(path: Path) -> Profile:
    """Read and validate a profile JSON. Raises ValueError on malformed input."""
    raw = path.expanduser().read_text(encoding="utf-8")
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError(f"not valid JSON: {exc}") from exc
    if not isinstance(data, dict):
        raise ValueError("profile root must be an object")
    values = data.get("values")
    if not isinstance(values, dict):
        raise ValueError("profile has no 'values' object")

    entries: list[ImportEntry] = []
    for code_str, payload in values.items():
        try:
            code = int(str(code_str), 16)
        except ValueError as exc:
            raise ValueError(f"invalid feature code {code_str!r}") from exc
        if not isinstance(payload, dict):
            raise ValueError(f"feature {code_str} entry must be an object")
        value = payload.get("value")
        if not isinstance(value, int) or isinstance(value, bool):
            raise ValueError(f"feature {code_str} value must be an integer")
        if not 0 <= value <= 0xFFFF:
            raise ValueError(f"feature {code_str} value {value} out of range")
        continuous = str(payload.get("type", "")) == ValueType.CONTINUOUS.value
        entries.append(ImportEntry(code=code, value=value, continuous=continuous))

    return Profile(
        monitor_id=str(data.get("monitor", "")),
        mccs_version=str(data.get("mccs_version", "")),
        entries=entries,
    )
