"""Strict JSON boundaries for okstra-owned artifacts."""
from __future__ import annotations

import json
import os
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path
import tempfile
from typing import Any

from .final_report_schema import SchemaError, validate as validate_schema


class JsonBoundaryError(ValueError):
    """An owned JSON artifact could not satisfy its persistence contract."""

    def __init__(self, path: Path, artifact: str, reason: str) -> None:
        self.path = path
        self.artifact = artifact
        self.reason = reason
        super().__init__(f"{artifact}: {reason}: {path}")


_EXTERNAL_SOURCE_SEAL = object()


@dataclass(frozen=True, init=False)
class ExternalJsonSource:
    """검증된 외부 생산자가 소유한 JSON 입력 경로다."""

    path: Path
    producer: str

    def __init__(self, path: Path, producer: str, seal: object = None) -> None:
        if seal is not _EXTERNAL_SOURCE_SEAL:
            raise TypeError("ExternalJsonSource is sealed")
        object.__setattr__(self, "path", path)
        object.__setattr__(self, "producer", producer)


@dataclass(frozen=True)
class OwnedJsonSnapshot:
    """한 번 읽은 원시 바이트와 그 바이트에서 검증한 객체다."""

    value: dict[str, Any]
    raw_bytes: bytes


def _issue_external_source(path: Path, producer: str) -> ExternalJsonSource:
    if not isinstance(path, Path):
        raise TypeError("external JSON source requires Path")
    return ExternalJsonSource(path, producer, _EXTERNAL_SOURCE_SEAL)


def _is_okstra_owned_path(path: Path) -> bool:
    lexical = Path(os.path.abspath(path))
    resolved = path.resolve(strict=False)
    return ".okstra" in lexical.parts or ".okstra" in resolved.parts


def external_user_json_source(path: Path) -> ExternalJsonSource:
    """사용자 제공 JSON이 Okstra 소유 트리 밖에 있을 때만 승인한다."""
    if _is_okstra_owned_path(path):
        raise JsonBoundaryError(path, "external JSON", "Okstra-owned path")
    return _issue_external_source(path, "user")


def external_worker_json_source(
    path: Path, *, trusted_root: Path, lane_root: Path
) -> ExternalJsonSource:
    """신뢰 루트 아래의 정확한 worker 결과 lane만 승인한다."""
    lexical_root = Path(os.path.abspath(trusted_root))
    lexical_lane = Path(os.path.abspath(lane_root))
    lexical_path = Path(os.path.abspath(path))
    try:
        lane_parts = lexical_lane.relative_to(lexical_root).parts
    except ValueError:
        lane_parts = ()
    is_worker_lane = (
        len(lane_parts) in {7, 8}
        and lane_parts[:2] == (".okstra", "tasks")
        and lane_parts[4] == "runs"
        and lane_parts[-1] == "worker-results"
        and (len(lane_parts) == 7 or lane_parts[6].startswith("stage-"))
    )
    if not is_worker_lane or lexical_path.parent != lexical_lane:
        raise JsonBoundaryError(
            path, "worker JSON", "outside worker result lanes"
        )
    try:
        lexical_lane.relative_to(lexical_root)
        lexical_path.relative_to(lexical_lane)
        resolved_lane = lane_root.resolve(strict=False)
        resolved_lane.relative_to(trusted_root.resolve(strict=False))
        path.resolve(strict=False).relative_to(resolved_lane)
    except ValueError as exc:
        raise JsonBoundaryError(
            path, "worker JSON", "outside worker result lanes"
        ) from exc
    if _has_symlink_component(lexical_lane, lexical_root) or _has_symlink_component(
        lexical_path, lexical_lane
    ):
        raise JsonBoundaryError(path, "worker JSON", "outside worker result lanes")
    return _issue_external_source(path, "worker")


def external_invocation_json_source(
    path: Path, *, project_root: Path, purpose: str
) -> ExternalJsonSource:
    """정확한 standalone purpose lane의 직접 결과만 승인한다."""
    if not purpose or any(char not in "abcdefghijklmnopqrstuvwxyz0123456789-" for char in purpose):
        raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
    lane = project_root / ".okstra" / "agent-invocations" / purpose
    lexical_path = Path(os.path.abspath(path))
    lexical_lane = Path(os.path.abspath(lane))
    lexical_root = Path(os.path.abspath(project_root))
    if lexical_path.parent != lexical_lane or path.is_symlink():
        raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
    try:
        lane.resolve(strict=False).relative_to(project_root.resolve(strict=False))
        path.resolve(strict=False).relative_to(lane.resolve(strict=False))
    except ValueError as exc:
        raise JsonBoundaryError(
            path, "invocation JSON", "outside invocation result lane"
        ) from exc
    if _has_symlink_component(lexical_lane, lexical_root):
        raise JsonBoundaryError(path, "invocation JSON", "outside invocation result lane")
    return _issue_external_source(path, "standalone worker")


def _has_symlink_component(path: Path, root: Path) -> bool:
    current = path
    while current != root:
        if current.is_symlink():
            return True
        if current.parent == current:
            return True
        current = current.parent
    return root.is_symlink()


def external_claude_global_json_source(
    path: Path, home_dir: Path
) -> ExternalJsonSource:
    """호스트 어댑터가 지정한 home의 전역 Claude 설정만 승인한다."""
    expected = home_dir / ".claude.json"
    if Path(os.path.abspath(path)) != Path(os.path.abspath(expected)) or path.is_symlink():
        raise JsonBoundaryError(path, "Claude JSON", "outside fixed adapter path")
    return _issue_external_source(path, "Claude Code")


def external_claude_team_json_source(path: Path) -> ExternalJsonSource:
    """현재 사용자의 Claude team roster 설정만 승인한다."""
    root = Path.home() / ".claude" / "teams"
    try:
        Path(os.path.abspath(path)).relative_to(Path(os.path.abspath(root)))
        path.resolve(strict=False).relative_to(root.resolve(strict=False))
    except ValueError as exc:
        raise JsonBoundaryError(path, "Claude JSON", "outside Claude team root") from exc
    if path.name != "config.json" or path.is_symlink():
        raise JsonBoundaryError(path, "Claude JSON", "outside Claude team root")
    return _issue_external_source(path, "Claude Code")


def external_tool_json_source(path: Path, workspace_root: Path) -> ExternalJsonSource:
    """외부 도구가 workspace의 비-Okstra 경로에 쓴 보고서만 승인한다."""
    try:
        path.resolve(strict=False).relative_to(workspace_root.resolve(strict=False))
    except ValueError as exc:
        raise JsonBoundaryError(path, "tool JSON", "outside tool workspace") from exc
    if _is_okstra_owned_path(path):
        raise JsonBoundaryError(path, "tool JSON", "Okstra-owned path")
    return _issue_external_source(path, "external tool")


def external_error_sidecar_source(path: Path) -> ExternalJsonSource:
    """worker-results lane 또는 Okstra 밖 레거시 sidecar만 승인한다."""
    if _is_okstra_owned_path(path):
        raise JsonBoundaryError(
            path, "worker error sidecar", "trusted worker lane is required"
        )
    return _issue_external_source(path, "legacy automation")


class _NonstandardJsonConstant(ValueError):
    """Raised by ``json.loads`` when a non-standard numeric token appears."""


def load_owned_object(
    path: Path,
    *,
    artifact: str,
    schema: Mapping[str, Any] | None = None,
    validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
) -> dict[str, Any]:
    """Load and validate one okstra-owned JSON object."""
    return load_owned_object_snapshot(
        path,
        artifact=artifact,
        schema=schema,
        validate_cross_fields=validate_cross_fields,
    ).value


def load_owned_object_snapshot(
    path: Path,
    *,
    artifact: str,
    schema: Mapping[str, Any] | None = None,
    validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
) -> OwnedJsonSnapshot:
    """한 번 읽은 바이트에서 객체를 파싱·검증해 함께 반환한다."""
    try:
        raw_bytes = path.read_bytes()
        value = json.loads(
            raw_bytes.decode("utf-8"),
            parse_constant=_reject_nonstandard_json_constant,
        )
    except FileNotFoundError as exc:
        raise JsonBoundaryError(path, artifact, "missing") from exc
    except UnicodeDecodeError as exc:
        raise JsonBoundaryError(path, artifact, "invalid JSON encoding") from exc
    except _NonstandardJsonConstant as exc:
        raise JsonBoundaryError(path, artifact, str(exc)) from exc
    except json.JSONDecodeError as exc:
        raise JsonBoundaryError(path, artifact, "invalid JSON") from exc
    except OSError as exc:
        raise JsonBoundaryError(path, artifact, f"I/O error while reading ({exc})") from exc
    return OwnedJsonSnapshot(
        value=_validate_owned(
            value,
            path=path,
            artifact=artifact,
            schema=schema,
            validate_cross_fields=validate_cross_fields,
        ),
        raw_bytes=raw_bytes,
    )


def load_external_json(
    source: ExternalJsonSource,
    *,
    artifact: str,
    object_pairs_hook: Callable[[list[tuple[str, Any]]], Any] | None = None,
) -> Any:
    """Load external JSON without applying an owned-artifact contract."""
    if not isinstance(source, ExternalJsonSource):
        raise TypeError("load_external_json requires ExternalJsonSource")
    path = source.path
    try:
        return json.loads(
            path.read_text(encoding="utf-8"),
            object_pairs_hook=object_pairs_hook,
        )
    except FileNotFoundError as exc:
        raise JsonBoundaryError(path, artifact, "missing") from exc
    except UnicodeDecodeError as exc:
        raise JsonBoundaryError(path, artifact, "invalid JSON encoding") from exc
    except json.JSONDecodeError as exc:
        raise JsonBoundaryError(path, artifact, "invalid JSON") from exc
    except OSError as exc:
        raise JsonBoundaryError(path, artifact, f"I/O error while reading ({exc})") from exc


def write_owned_object_atomic(
    path: Path,
    payload: Mapping[str, Any],
    *,
    artifact: str,
    schema: Mapping[str, Any] | None = None,
    validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
) -> None:
    """Validate a payload before atomically replacing an owned artifact."""
    serialized = serialize_owned_object(
        path,
        payload,
        artifact=artifact,
        schema=schema,
        validate_cross_fields=validate_cross_fields,
    )
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        descriptor, raw_temp = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    except OSError as exc:
        raise JsonBoundaryError(path, artifact, f"I/O error while preparing write ({exc})") from exc
    temporary = Path(raw_temp)
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
            handle.write(serialized)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    except (TypeError, ValueError) as exc:
        raise JsonBoundaryError(path, artifact, f"JSON serialization failed ({exc})") from exc
    except OSError as exc:
        raise JsonBoundaryError(path, artifact, f"I/O error while writing ({exc})") from exc
    finally:
        temporary.unlink(missing_ok=True)


def serialize_owned_object(
    path: Path,
    payload: Mapping[str, Any],
    *,
    artifact: str,
    schema: Mapping[str, Any] | None = None,
    validate_cross_fields: Callable[[dict[str, Any]], object] | None = None,
) -> str:
    """전용 게시 프로토콜이 사용할 검증된 canonical 객체 문자열이다."""
    owned = _validate_owned(
        payload, path=path, artifact=artifact, schema=schema,
        validate_cross_fields=validate_cross_fields,
    )
    try:
        return json.dumps(
            owned, ensure_ascii=False, indent=2, allow_nan=False
        ) + "\n"
    except (TypeError, ValueError) as exc:
        raise JsonBoundaryError(
            path, artifact, f"JSON serialization failed ({exc})"
        ) from exc


def mutate_owned_object_atomic(
    path: Path,
    mutation: Callable[[dict[str, Any]], dict[str, Any]],
    **contract: Any,
) -> dict[str, Any]:
    """Load, mutate, validate, and atomically replace one owned object."""
    payload = load_owned_object(path, **contract)
    updated = mutation(dict(payload))
    write_owned_object_atomic(path, updated, **contract)
    return updated


def _reject_nonstandard_json_constant(constant: str) -> None:
    raise _NonstandardJsonConstant(f"non-standard JSON constant: {constant}")


def _validate_owned(
    value: object,
    *,
    path: Path,
    artifact: str,
    schema: Mapping[str, Any] | None,
    validate_cross_fields: Callable[[dict[str, Any]], object] | None,
) -> dict[str, Any]:
    if not isinstance(value, Mapping):
        raise JsonBoundaryError(path, artifact, "top-level JSON value must be an object")
    owned = dict(value)
    if schema is not None:
        try:
            schema_errors = validate_schema(owned, dict(schema))
        except SchemaError as exc:
            raise JsonBoundaryError(path, artifact, f"invalid schema ({exc})") from exc
        if schema_errors:
            raise JsonBoundaryError(path, artifact, f"schema validation failed ({schema_errors[0]})")
    if validate_cross_fields is not None:
        errors = validate_cross_fields(owned)
        if isinstance(errors, str):
            errors = [errors]
        if errors:
            raise JsonBoundaryError(path, artifact, f"cross-field validation failed ({next(iter(errors))})")
    return owned
