from __future__ import annotations

import json
from pathlib import Path
from typing import Any


SURFACE_TYPES = {"profile", "action"}
SURFACE_DOMAINS = {"testing", "automation", "deployment"}
SURFACE_VISIBILITIES = {"machine-only", "machine-preferred", "human-and-machine"}


def load_preset(path: Path | str) -> dict[str, Any]:
    preset_path = Path(path).resolve()
    payload = json.loads(preset_path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError("preset root must be an object")
    normalized = _normalize_preset(payload)
    normalized["_preset_path"] = str(preset_path)
    return normalized


def manifest_path_from_preset(preset: dict[str, Any]) -> Path:
    project = preset.get("project")
    if not isinstance(project, dict):
        raise ValueError("preset is missing normalized `project` metadata")
    manifest = project.get("manifest")
    if not isinstance(manifest, str) or not manifest.strip():
        raise ValueError("preset is missing normalized `project.manifest` metadata")
    return (repo_root_from_preset(preset) / manifest.strip()).resolve()


def preset_profile_names(preset: dict[str, Any]) -> list[str]:
    return sorted(
        surface["name"]
        for surface in preset["surfaces"]
        if surface["type"] == "profile"
    )


def repo_root_from_preset(preset: dict[str, Any]) -> Path:
    preset_path = preset.get("_preset_path")
    if not isinstance(preset_path, str) or not preset_path.strip():
        raise ValueError("preset is missing internal `_preset_path` metadata")
    return Path(preset_path).resolve().parent.parent.parent


def _normalize_preset(payload: dict[str, Any]) -> dict[str, Any]:
    schema_version = _read_schema_version(payload)
    if schema_version == 1:
        return _normalize_v1_preset(payload)
    if schema_version == 2:
        return _normalize_v2_preset(payload)
    raise ValueError(f"unsupported preset schema_version: {schema_version}")


def _read_schema_version(payload: dict[str, Any]) -> int:
    raw = payload.get("schema_version")
    if raw is None:
        return 1
    if isinstance(raw, bool) or not isinstance(raw, int):
        raise ValueError("preset `schema_version` must be an integer")
    if raw not in {1, 2}:
        raise ValueError(f"unsupported preset schema_version: {raw}")
    return raw


def _normalize_v1_preset(payload: dict[str, Any]) -> dict[str, Any]:
    project_name = _read_required_string(payload, "name")
    runtime = _read_required_object(payload, "runtime")
    manifest = _read_required_string(runtime, "manifest", "preset `runtime.manifest`")

    testing = _read_required_object(runtime, "testing", "preset `runtime.testing`")
    profiles = _read_required_object(
        testing,
        "profiles",
        "preset `runtime.testing.profiles`",
        require_non_empty=True,
    )

    deployment = _read_required_object(runtime, "deployment", "preset `runtime.deployment`")
    actions = _read_required_object(
        deployment,
        "actions",
        "preset `runtime.deployment.actions`",
        require_non_empty=True,
    )

    surfaces: list[dict[str, str]] = []
    for surface_name, target in profiles.items():
        target_name = _validate_non_empty_string(
            target,
            f"preset `runtime.testing.profiles.{surface_name}`",
        )
        normalized_name = _validate_non_empty_string(
            surface_name,
            "preset testing profile name",
        )
        surfaces.append(
            {
                "name": normalized_name,
                "type": "profile",
                "target": target_name,
                "domain": "automation" if normalized_name.endswith(".workflow") else "testing",
                "visibility": (
                    "machine-preferred"
                    if normalized_name == "code-quality.workflow"
                    else "human-and-machine"
                ),
            }
        )

    for surface_name, action in actions.items():
        normalized_name = _validate_non_empty_string(
            surface_name,
            "preset deployment action name",
        )
        action_payload = _read_required_object(
            action,
            "",
            f"preset `runtime.deployment.actions.{surface_name}`",
        )
        impl = _read_required_string(
            action_payload,
            "impl",
            f"preset `runtime.deployment.actions.{surface_name}.impl`",
        )
        surfaces.append(
            {
                "name": normalized_name,
                "type": "action",
                "impl": impl,
                "domain": "deployment",
                "visibility": "human-and-machine",
            }
        )

    normalized_surfaces = _normalize_surface_list(surfaces)
    return {
        "schema_version": 1,
        "project": {
            "name": project_name,
            "manifest": manifest,
        },
        "surfaces": normalized_surfaces,
        "surface_index": _build_surface_index(normalized_surfaces),
        "entrypoints": _normalize_entrypoints(payload.get("entrypoints")),
    }


def _normalize_v2_preset(payload: dict[str, Any]) -> dict[str, Any]:
    project = _read_required_object(payload, "project", "preset `project`")
    normalized_surfaces = _normalize_surface_list(
        _read_required_list(payload, "surfaces", "preset `surfaces`"),
    )
    return {
        "schema_version": 2,
        "project": {
            "name": _read_required_string(project, "name", "preset `project.name`"),
            "manifest": _read_required_string(project, "manifest", "preset `project.manifest`"),
        },
        "surfaces": normalized_surfaces,
        "surface_index": _build_surface_index(normalized_surfaces),
        "entrypoints": _normalize_entrypoints(payload.get("entrypoints")),
    }


def _normalize_surface_list(raw_surfaces: list[Any]) -> list[dict[str, str]]:
    if not raw_surfaces:
        raise ValueError("preset `surfaces` must be a non-empty array")

    normalized_surfaces: list[dict[str, str]] = []
    for index, surface in enumerate(raw_surfaces):
        label = f"preset `surfaces[{index}]`"
        surface_payload = _read_required_object(surface, "", label)
        normalized_name = _read_required_string(surface_payload, "name", f"{label}.name")
        surface_type = _read_required_string(surface_payload, "type", f"{label}.type")
        if surface_type not in SURFACE_TYPES:
            raise ValueError(f"{label}.type must be one of {sorted(SURFACE_TYPES)}")

        domain = _read_required_string(surface_payload, "domain", f"{label}.domain")
        if domain not in SURFACE_DOMAINS:
            raise ValueError(f"{label}.domain must be one of {sorted(SURFACE_DOMAINS)}")

        visibility = _read_required_string(surface_payload, "visibility", f"{label}.visibility")
        if visibility not in SURFACE_VISIBILITIES:
            raise ValueError(f"{label}.visibility must be one of {sorted(SURFACE_VISIBILITIES)}")

        normalized_surface: dict[str, str] = {
            "name": normalized_name,
            "type": surface_type,
            "domain": domain,
            "visibility": visibility,
        }
        if surface_type == "profile":
            normalized_surface["target"] = _read_required_string(surface_payload, "target", f"{label}.target")
        if surface_type == "action":
            normalized_surface["impl"] = _read_required_string(surface_payload, "impl", f"{label}.impl")
        normalized_surfaces.append(normalized_surface)

    return normalized_surfaces


def _build_surface_index(surfaces: list[dict[str, str]]) -> dict[str, dict[str, str]]:
    index: dict[str, dict[str, str]] = {}
    for surface in surfaces:
        surface_name = surface["name"]
        if surface_name in index:
            raise ValueError(f"preset surface `{surface_name}` is duplicated")
        index[surface_name] = dict(surface)
    return index


def _normalize_entrypoints(raw_entrypoints: Any) -> dict[str, str]:
    if raw_entrypoints is None:
        return {}
    if not isinstance(raw_entrypoints, dict):
        raise ValueError("preset `entrypoints` must be an object when provided")

    entrypoints: dict[str, str] = {}
    for key, value in raw_entrypoints.items():
        entrypoint_name = _validate_non_empty_string(key, "preset entrypoint name")
        entrypoints[entrypoint_name] = _validate_non_empty_string(
            value,
            f"preset `entrypoints.{entrypoint_name}`",
        )
    return entrypoints


def _read_required_object(
    payload: Any,
    key: str,
    error_label: str | None = None,
    *,
    require_non_empty: bool = False,
) -> dict[str, Any]:
    value = payload if key == "" else payload.get(key)
    label = error_label or f"preset `{key}`"
    if not isinstance(value, dict):
        raise ValueError(f"{label} must be an object")
    if require_non_empty and not value:
        raise ValueError(f"{label} must be a non-empty object")
    return value


def _read_required_list(payload: dict[str, Any], key: str, error_label: str | None = None) -> list[Any]:
    value = payload.get(key)
    label = error_label or f"preset `{key}`"
    if not isinstance(value, list):
        raise ValueError(f"{label} must be an array")
    return value


def _read_required_string(payload: dict[str, Any], key: str, error_label: str | None = None) -> str:
    return _validate_non_empty_string(payload.get(key), error_label or f"preset `{key}`")


def _validate_non_empty_string(value: Any, label: str) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ValueError(f"{label} must be a non-empty string")
    return value.strip()
