"""Root project entity — what a Studio ``.appack`` package contains.

A ``ProjectConfig`` is the unit of work in the Studio: one customer
project = one configured vessel. It serialises to YAML or JSON for
transport into the firmware build pipeline.
"""

from __future__ import annotations

import json
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, ConfigDict, Field

from arautopilot.core.ids import ProjectId, new_project_id
from arautopilot.core.vessel_config import VesselConfig
from arautopilot.version import __version__


class ProjectConfig(BaseModel):
    """Root configuration object persisted to disk and shipped as ``.appack``."""

    model_config = ConfigDict(extra="forbid", validate_assignment=True)

    schema_version: str = Field(default="0.1.0", pattern=r"^\d+\.\d+\.\d+$")
    """Project file schema version (independent of package ``__version__``)."""

    package_version: str = Field(default=__version__)
    """Version of ``arautopilot`` that produced this file (informational)."""

    project_id: ProjectId = Field(default_factory=new_project_id)
    client_name: str = Field(min_length=1, max_length=120)
    project_name: str = Field(min_length=1, max_length=120)
    notes: str = Field(default="", max_length=2000)

    vessel: VesselConfig

    created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
    modified_at: datetime = Field(default_factory=lambda: datetime.now(UTC))

    # --- Serialisation -------------------------------------------------------
    def to_dict(self) -> dict[str, Any]:
        """Return a JSON-ready dict (datetimes as ISO 8601 strings)."""
        return self.model_dump(mode="json")

    def to_json(self, *, indent: int | None = 2) -> str:
        """Serialise to JSON text."""
        return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)

    def to_yaml(self) -> str:
        """Serialise to YAML text (block style, sorted keys disabled to preserve schema order)."""
        return yaml.safe_dump(
            self.to_dict(),
            sort_keys=False,
            default_flow_style=False,
            allow_unicode=True,
        )

    def save_yaml(self, path: Path | str) -> Path:
        """Write the project to ``path`` as YAML and return the resolved path."""
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(self.to_yaml(), encoding="utf-8")
        return p

    def save_json(self, path: Path | str) -> Path:
        """Write the project to ``path`` as JSON and return the resolved path."""
        p = Path(path)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(self.to_json(), encoding="utf-8")
        return p

    # --- Deserialisation -----------------------------------------------------
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
        return cls.model_validate(data)

    @classmethod
    def from_json(cls, text: str) -> ProjectConfig:
        return cls.model_validate(json.loads(text))

    @classmethod
    def from_yaml(cls, text: str) -> ProjectConfig:
        return cls.model_validate(yaml.safe_load(text))

    @classmethod
    def load(cls, path: Path | str) -> ProjectConfig:
        """Load from disk; format inferred from the file extension."""
        p = Path(path)
        text = p.read_text(encoding="utf-8")
        suffix = p.suffix.lower()
        if suffix in (".yaml", ".yml"):
            return cls.from_yaml(text)
        if suffix == ".json":
            return cls.from_json(text)
        raise ValueError(f"Unsupported project file extension: {suffix!r}")

    def touch(self) -> ProjectConfig:
        """Return a copy with ``modified_at`` refreshed to now (UTC)."""
        return self.model_copy(update={"modified_at": datetime.now(UTC)})
