from __future__ import annotations

import errno
import os
import tempfile
import threading
import time
from pathlib import Path
from typing import Optional, TYPE_CHECKING

from api.errors import ApiError, ErrorCode
from engine.pipeline.telemetry import timer

if TYPE_CHECKING:  # pragma: no cover - typing helper
    from ops.kms.base import EnvelopeKMS

__all__ = [
    "write_bytes",
    "write_text",
    "write_bytes_atomic",
    "write_text_atomic",
    "read_bytes",
    "read_text",
    "kms_enabled",
    "reset_storage_adapter",
]

_ADAPTER: Optional["EnvelopeKMS"] = None
_LOCK = threading.Lock()
_WINDOWS_SHARING_ERRNOS = {errno.EACCES, errno.EPERM}
if hasattr(errno, "ESHARE"):  # pragma: no cover - Windows only
    _WINDOWS_SHARING_ERRNOS.add(errno.ESHARE)


def _fsync_dir(path: Path) -> None:
    """Fsync the parent directory for durability of the rename."""
    dir_fd: Optional[int] = None
    try:
        flags = os.O_RDONLY
        if hasattr(os, "O_DIRECTORY"):
            flags |= os.O_DIRECTORY  # type: ignore[attr-defined]
        dir_fd = os.open(str(path.parent), flags)
    except (FileNotFoundError, NotADirectoryError, PermissionError, OSError):
        return

    try:
        with timer("fsync"):
            os.fsync(dir_fd)
    except OSError:
        pass
    finally:
        if dir_fd is not None:
            try:
                os.close(dir_fd)
            except OSError:
                pass


def _write_all(fd: int, data: bytes) -> None:
    """Loop on os.write to avoid partial writes on some filesystems."""
    view = memoryview(data)
    offset = 0
    total = len(view)
    while offset < total:
        written = os.write(fd, view[offset:])
        if written <= 0:
            raise OSError(errno.EIO, "Short write while writing receipt")
        offset += written


def kms_enabled() -> bool:
    return _resolve_adapter() is not None


def reset_storage_adapter() -> None:
    global _ADAPTER
    with _LOCK:
        _ADAPTER = None


def write_bytes(path: Path, data: bytes, tenant: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    adapter = _resolve_adapter()
    if adapter is None:
        path.write_bytes(data)
        return
    try:
        envelope = adapter.encrypt(tenant, data)
    except Exception as exc:  # pragma: no cover - surfaced in API layer
        raise ApiError(
            code=ErrorCode.KMS_ENCRYPT_FAIL,
            status=500,
            title="KMS encryption failed",
            detail=f"KMS encryption failed for tenant '{tenant}': unable to encrypt payload.",
        ) from exc
    path.write_bytes(envelope)


def write_text(path: Path, text: str, tenant: str) -> None:
    write_bytes(path, text.encode("utf-8"), tenant)


def write_bytes_atomic(path: Path, data: bytes, tenant: str) -> None:
    """
    Atomic write with fsync guarantee - no partial files on crash.
    
    Uses tempfile + fsync + atomic rename pattern to ensure durability.
    Critical for receipt integrity as legal artifacts.
    
    Args:
        path: Destination file path
        data: Raw bytes to write
        tenant: Tenant identifier for KMS encryption
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    adapter = _resolve_adapter()

    # Encrypt if KMS enabled
    if adapter is not None:
        try:
            data = adapter.encrypt(tenant, data)
        except Exception as exc:  # pragma: no cover - surfaced in API layer
            raise ApiError(
                code=ErrorCode.KMS_ENCRYPT_FAIL,
                status=500,
                title="KMS encryption failed",
                detail=f"KMS encryption failed for tenant '{tenant}': unable to encrypt payload.",
            ) from exc
    
    # Write to temp file in same directory (ensures same filesystem for atomic rename)
    fd: Optional[int] = None
    tmp_path: Optional[Path] = None
    try:
        fd, tmp_path_str = tempfile.mkstemp(
            dir=path.parent,
            prefix=f".{path.name}.",
            suffix=".tmp"
        )
        tmp_path = Path(tmp_path_str)

        _write_all(fd, data)

        # Force to disk (critical for durability)
        with timer("fsync"):
            os.fsync(fd)

        # Close before rename
        os.close(fd)
        fd = None

        # Atomic replace with limited retry for Windows sharing violations
        max_attempts = 1 if os.name != "nt" else 5
        last_error: Optional[OSError] = None
        for attempt in range(1, max_attempts + 1):
            try:
                os.replace(str(tmp_path), str(path))
                tmp_path = None
                break
            except OSError as exc:
                last_error = exc
                if os.name == "nt" and exc.errno in _WINDOWS_SHARING_ERRNOS:
                    time.sleep(0.05 * attempt)
                    continue
                raise
        else:
            assert last_error is not None  # for mypy
            raise last_error

        # Flush the directory entry so the rename is durable.
        _fsync_dir(path)

    except Exception:
        # Cleanup on failure
        if fd is not None:
            try:
                os.close(fd)
            except Exception:
                pass
        if tmp_path is not None and tmp_path.exists():
            try:
                tmp_path.unlink()
            except Exception:
                pass
        raise


def write_text_atomic(path: Path, text: str, tenant: str) -> None:
    """Atomic text write with fsync guarantee."""
    write_bytes_atomic(path, text.encode("utf-8"), tenant)


def read_bytes(path: Path, tenant_hint: Optional[str] = None) -> bytes:
    data = path.read_bytes()
    adapter = _resolve_adapter()
    if adapter is None or not _looks_like_envelope(data):
        return data
    try:
        return adapter.decrypt(tenant_hint, data)
    except Exception as exc:  # pragma: no cover - surfaced upstream
        raise ApiError(
            code=ErrorCode.KMS_DECRYPT_FAIL,
            status=500,
            title="KMS decrypt failed",
            detail=f"Unable to decrypt payload for tenant '{tenant_hint or 'unknown'}'.",
        ) from exc


def read_text(path: Path, tenant_hint: Optional[str] = None) -> str:
    return read_bytes(path, tenant_hint=tenant_hint).decode("utf-8")


def _looks_like_envelope(data: bytes) -> bool:
    return data.startswith(b"{") and b"\"data_key_id\"" in data and b"\"provider\"" in data


def _resolve_adapter() -> Optional["EnvelopeKMS"]:
    global _ADAPTER
    if _ADAPTER is not None:
        return _ADAPTER
    provider = os.getenv("LOOM_KMS_PROVIDER", "").strip().lower()
    if not provider:
        return None
    with _LOCK:
        if _ADAPTER is not None:
            return _ADAPTER
        if provider == "aws":
            from ops.kms.aws_kms import get_adapter

            _ADAPTER = get_adapter()
        elif provider == "gcp":
            from ops.kms.gcp_kms import get_adapter

            _ADAPTER = get_adapter()
        elif provider == "azure":
            from ops.kms.azure_kms import get_adapter

            _ADAPTER = get_adapter()
        else:
            _ADAPTER = None
        return _ADAPTER
