from __future__ import annotations

import json
import os
import tempfile
from pathlib import Path

from .models import ControlError, Project


class ProjectRegistry:
    """One Gateway writer; atomic replacement, no filesystem project discovery."""

    def __init__(self, path: str | Path):
        self.path = Path(path).expanduser()

    def list(self) -> list[Project]:
        if not self.path.exists():
            return []
        try:
            projects = [Project.model_validate(p) for p in json.loads(self.path.read_text())["projects"]]
            if len({p.id for p in projects}) != len(projects):
                raise ValueError("duplicate project id")
            return projects
        except (ValueError, KeyError, OSError) as exc:
            raise ControlError("invalid project registry; repair the configuration file") from exc

    def get(self, project_id: str) -> Project:
        for project in self.list():
            if project.id == project_id:
                return project
        raise ControlError("project not found", 404)

    def save(self, projects: list[Project]) -> None:
        self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
        fd, temporary = tempfile.mkstemp(dir=self.path.parent, prefix=".projects-")
        try:
            with os.fdopen(fd, "w") as handle:
                json.dump({"projects": [p.model_dump() for p in projects]}, handle, indent=2)
                handle.write("\n")
                handle.flush()
                os.fsync(handle.fileno())
            os.replace(temporary, self.path)
        finally:
            Path(temporary).unlink(missing_ok=True)

    def add(self, project: Project) -> Project:
        path = Path(project.path).expanduser()
        if not path.is_absolute() or not path.is_dir():
            raise ControlError("project path must be an existing absolute directory", 422)
        project = project.model_copy(update={"path": str(path.resolve())})
        projects = self.list()
        if any(p.id == project.id or p.path == project.path for p in projects):
            raise ControlError("project id or path already registered", 409)
        self.save([*projects, project])
        return project

    def delete(self, project_id: str) -> None:
        self.get(project_id)
        self.save([p for p in self.list() if p.id != project_id])
