"""Bounded atomic persistence for signed license documents."""

from __future__ import annotations

import os
from pathlib import Path
import tempfile
from typing import Optional

from .atomic_file import (
    DEFAULT_REPLACE_RETRY_COUNT,
    DEFAULT_REPLACE_RETRY_DELAY_SECONDS,
    replace_file_with_retry,
    validate_replace_retry_parameters,
)
from .canonical_json import canonicalize_json, parse_bounded_json
from .constants import MAX_SIGNED_DOCUMENT_BYTES
from .errors import LicenseErrorCode, LicensingError
from .models import SignedLicenseDocument


SIGNED_DOCUMENT_LINE_TERMINATOR = b"\r\n"


class SignedDocumentStore:
    def __init__(
        self,
        path: str | Path,
        *,
        maximum_bytes: int = MAX_SIGNED_DOCUMENT_BYTES,
        replace_retry_count: int = DEFAULT_REPLACE_RETRY_COUNT,
        replace_retry_delay_seconds: float = DEFAULT_REPLACE_RETRY_DELAY_SECONDS,
    ) -> None:
        if isinstance(path, bytes) or not isinstance(path, (str, os.PathLike)):
            raise TypeError("path must be a string or path-like value")
        resolved_path = Path(path)
        if not resolved_path.name:
            raise ValueError("path must identify a file")
        if isinstance(maximum_bytes, bool) or not isinstance(maximum_bytes, int):
            raise TypeError("maximum_bytes must be an integer")
        if maximum_bytes < 1:
            raise ValueError("maximum_bytes must be at least one")
        retry_count, retry_delay = validate_replace_retry_parameters(
            replace_retry_count,
            replace_retry_delay_seconds,
        )
        self.path = resolved_path
        self.maximum_bytes = maximum_bytes
        self.replace_retry_count = retry_count
        self.replace_retry_delay_seconds = retry_delay

    def load(self) -> Optional[SignedLicenseDocument]:
        if not self.path.exists():
            return None
        try:
            if not self.path.is_file() or self.path.is_symlink():
                raise LicensingError(
                    LicenseErrorCode.STORAGE_IO_ERROR,
                    "signed-license path is not a regular file",
                    path=self.path,
                )
            size = self.path.stat().st_size
            if size > self.maximum_bytes:
                raise LicensingError(
                    LicenseErrorCode.DOCUMENT_TOO_LARGE,
                    f"signed-license file exceeds {self.maximum_bytes} bytes",
                    path=self.path,
                )
            raw = self.path.read_bytes()
            value = parse_bounded_json(raw, maximum_bytes=self.maximum_bytes)
            return SignedLicenseDocument.from_mapping(value)
        except LicensingError:
            raise
        except OSError as exc:
            raise LicensingError(
                LicenseErrorCode.STORAGE_IO_ERROR,
                f"could not read signed-license file: {exc}",
                path=self.path,
            ) from exc

    def save(self, document: SignedLicenseDocument) -> None:
        if not isinstance(document, SignedLicenseDocument):
            raise TypeError("document must be a SignedLicenseDocument")
        payload = canonicalize_json(document.to_mapping()) + SIGNED_DOCUMENT_LINE_TERMINATOR
        if len(payload) > self.maximum_bytes:
            raise LicensingError(
                LicenseErrorCode.DOCUMENT_TOO_LARGE,
                f"signed-license document exceeds {self.maximum_bytes} bytes",
                path=self.path,
            )
        temporary_path: Optional[Path] = None
        try:
            self.path.parent.mkdir(parents=True, exist_ok=True)
            descriptor, temporary_name = tempfile.mkstemp(
                prefix=f".{self.path.name}.",
                suffix=".tmp",
                dir=self.path.parent,
            )
            temporary_path = Path(temporary_name)
            with os.fdopen(descriptor, "wb") as stream:
                stream.write(payload)
                stream.flush()
                os.fsync(stream.fileno())
            replace_file_with_retry(
                temporary_path,
                self.path,
                retry_count=self.replace_retry_count,
                retry_delay_seconds=self.replace_retry_delay_seconds,
            )
            temporary_path = None
        except LicensingError:
            raise
        except OSError as exc:
            raise LicensingError(
                LicenseErrorCode.STORAGE_IO_ERROR,
                f"could not publish signed-license file: {exc}",
                path=self.path,
            ) from exc
        finally:
            if temporary_path is not None:
                try:
                    temporary_path.unlink(missing_ok=True)
                except OSError:
                    pass

    def delete(self) -> None:
        try:
            self.path.unlink(missing_ok=True)
        except OSError as exc:
            raise LicensingError(
                LicenseErrorCode.STORAGE_IO_ERROR,
                f"could not remove signed-license file: {exc}",
                path=self.path,
            ) from exc


__all__ = ["SIGNED_DOCUMENT_LINE_TERMINATOR", "SignedDocumentStore"]
