from __future__ import annotations

import os
import re
import tempfile
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
MAX_NAME_LENGTH = 240


class InvalidNameError(Exception):
    pass


@dataclass
class SiteInfo:
    name: str
    size: int
    modified: str


class SiteStorage:
    def __init__(self, sites_dir: str | Path):
        self.sites_dir = Path(sites_dir)
        self.sites_dir.mkdir(parents=True, exist_ok=True)

    @staticmethod
    def validate_name(name: str) -> None:
        if len(name) > MAX_NAME_LENGTH:
            raise InvalidNameError(
                f"Site name too long ({len(name)} chars). Maximum is {MAX_NAME_LENGTH}."
            )
        if not NAME_RE.match(name):
            raise InvalidNameError(
                f"Invalid site name: {name!r}. "
                "Must start with alphanumeric and contain only alphanumeric, hyphens, underscores."
            )

    def _path(self, name: str) -> Path:
        return self.sites_dir / f"{name}.html"

    def exists(self, name: str) -> bool:
        self.validate_name(name)
        return self._path(name).exists()

    def put(self, name: str, content: str) -> SiteInfo:
        self.validate_name(name)
        path = self._path(name)
        tmp_path = None
        try:
            fd, tmp_path = tempfile.mkstemp(dir=str(self.sites_dir), suffix=".tmp")
            with os.fdopen(fd, "w", encoding="utf-8") as f:
                f.write(content)
            os.replace(tmp_path, path)
            tmp_path = None
        except OSError as e:
            raise OSError(f"Failed to write site {name!r}: {e}") from e
        finally:
            if tmp_path is not None:
                try:
                    os.unlink(tmp_path)
                except OSError:
                    pass
        return self._info(name, path)

    def get(self, name: str) -> str | None:
        self.validate_name(name)
        path = self._path(name)
        if not path.exists():
            return None
        return path.read_text(encoding="utf-8")

    def delete(self, name: str) -> bool:
        self.validate_name(name)
        path = self._path(name)
        if not path.exists():
            return False
        path.unlink()
        return True

    def list(self) -> list[SiteInfo]:
        sites = []
        for path in sorted(self.sites_dir.glob("*.html")):
            name = path.stem
            sites.append(self._info(name, path))
        return sites

    def _info(self, name: str, path: Path) -> SiteInfo:
        stat = path.stat()
        modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
        return SiteInfo(name=name, size=stat.st_size, modified=modified)
