"""Loopback-only, checksum-validated PBF acquisition for Phase 2A.3 tests."""

from __future__ import annotations

import hashlib
import json
import os
import stat
import tempfile
import urllib.error
import urllib.request
from collections.abc import Callable
from datetime import datetime, timezone
from pathlib import Path
from typing import IO, cast

from ..errors import PbfDownloadRejected
from .models import ApprovedTestPbfDownload, ValidatedPbfDownload

_CHUNK_SIZE = 64 * 1024
_MAX_CACHE_METADATA_BYTES = 16 * 1024
_MAX_HEADER_VALUE_LENGTH = 1024
_CACHE_METADATA_SCHEMA_VERSION = 1
_ALLOWED_CONTENT_TYPES = frozenset(
    {"application/octet-stream", "binary/octet-stream", "application/x-protobuf"}
)
_CACHE_METADATA_KEYS = frozenset(
    {
        "schema_version",
        "region_id",
        "source_url",
        "content_sha256",
        "size_bytes",
        "downloaded_at",
        "etag",
        "last_modified",
    }
)


def _utc_now() -> datetime:
    return datetime.now(timezone.utc)


def _clock_value(clock: Callable[[], datetime], field_name: str) -> datetime:
    value = clock()
    if not isinstance(value, datetime) or value.tzinfo is None:
        raise PbfDownloadRejected(f"{field_name} clock value must be timezone-aware")
    return value.astimezone(timezone.utc)


class _RejectRedirects(urllib.request.HTTPRedirectHandler):
    def redirect_request(
        self,
        req: urllib.request.Request,
        fp: IO[bytes],
        code: int,
        msg: str,
        headers: object,
        newurl: str,
    ) -> urllib.request.Request | None:
        del req, fp, code, msg, headers, newurl
        return None


def _test_opener() -> urllib.request.OpenerDirector:
    return urllib.request.build_opener(
        urllib.request.ProxyHandler({}),
        _RejectRedirects(),
    )


def _validate_cache_directory(cache_dir: Path) -> None:
    if not isinstance(cache_dir, Path):
        raise PbfDownloadRejected("cache_dir must be pathlib.Path")
    try:
        info = cache_dir.lstat()
    except OSError as exc:
        raise PbfDownloadRejected("cache directory does not exist") from exc
    if stat.S_ISLNK(info.st_mode):
        raise PbfDownloadRejected("cache directory cannot be a symbolic link")
    if not stat.S_ISDIR(info.st_mode):
        raise PbfDownloadRejected("cache path must be a directory")


def _stat_identity(result: os.stat_result) -> tuple[int, int, int, int, int]:
    return (
        result.st_dev,
        result.st_ino,
        result.st_size,
        result.st_mtime_ns,
        result.st_ctime_ns,
    )


def _validate_cached_file(path: Path, settings: ApprovedTestPbfDownload) -> int:
    try:
        before = path.lstat()
    except OSError as exc:
        raise PbfDownloadRejected("cached PBF cannot be inspected") from exc
    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
        raise PbfDownloadRejected("cached PBF must be a regular file")
    if before.st_size != settings.expected_size_bytes:
        raise PbfDownloadRejected("cached PBF size does not match approved metadata")

    digest = hashlib.sha256()
    total = 0
    try:
        with path.open("rb") as handle:
            while chunk := handle.read(_CHUNK_SIZE):
                total += len(chunk)
                if total > settings.max_size_bytes:
                    raise PbfDownloadRejected("cached PBF exceeds approved size limit")
                digest.update(chunk)
        after = path.lstat()
    except OSError as exc:
        raise PbfDownloadRejected("cached PBF cannot be read") from exc
    if stat.S_ISLNK(after.st_mode) or not stat.S_ISREG(after.st_mode):
        raise PbfDownloadRejected("cached PBF changed during validation")
    if _stat_identity(before) != _stat_identity(after):
        raise PbfDownloadRejected("cached PBF changed during validation")
    if total != settings.expected_size_bytes:
        raise PbfDownloadRejected("cached PBF byte count does not match approved metadata")
    if digest.hexdigest() != settings.expected_sha256:
        raise PbfDownloadRejected("cached PBF checksum does not match approved metadata")
    return total


def _cache_metadata_path(target: Path) -> Path:
    return target.with_name(f"{target.name}.metadata.json")


def _isoformat_utc(value: datetime) -> str:
    return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")


def _parse_utc_timestamp(value: object) -> datetime:
    if not isinstance(value, str) or not value:
        raise PbfDownloadRejected("cached PBF downloaded_at metadata is invalid")
    normalized = f"{value[:-1]}+00:00" if value.endswith("Z") else value
    try:
        parsed = datetime.fromisoformat(normalized)
    except ValueError:
        raise PbfDownloadRejected("cached PBF downloaded_at metadata is invalid") from None
    if parsed.tzinfo is None:
        raise PbfDownloadRejected("cached PBF downloaded_at metadata must be timezone-aware")
    return parsed.astimezone(timezone.utc)


def _optional_metadata_string(value: object, field_name: str) -> str | None:
    if value is None:
        return None
    if not isinstance(value, str):
        raise PbfDownloadRejected(f"cached PBF {field_name} metadata is invalid")
    if len(value) > _MAX_HEADER_VALUE_LENGTH or "\r" in value or "\n" in value:
        raise PbfDownloadRejected(f"cached PBF {field_name} metadata is invalid")
    return value


def _serialize_cache_metadata(
    settings: ApprovedTestPbfDownload,
    downloaded_at: datetime,
    etag: str | None,
    last_modified: str | None,
) -> bytes:
    payload: dict[str, object] = {
        "schema_version": _CACHE_METADATA_SCHEMA_VERSION,
        "region_id": settings.region_id,
        "source_url": settings.url,
        "content_sha256": settings.expected_sha256,
        "size_bytes": settings.expected_size_bytes,
        "downloaded_at": _isoformat_utc(downloaded_at),
        "etag": etag,
        "last_modified": last_modified,
    }
    encoded = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
    if len(encoded) > _MAX_CACHE_METADATA_BYTES:
        raise PbfDownloadRejected("cache metadata exceeds the bounded size")
    return encoded


def _read_cache_metadata(
    path: Path,
    settings: ApprovedTestPbfDownload,
) -> tuple[datetime, str | None, str | None]:
    try:
        before = path.lstat()
    except OSError as exc:
        raise PbfDownloadRejected("cached PBF metadata cannot be inspected") from exc
    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
        raise PbfDownloadRejected("cached PBF metadata must be a regular file")
    if before.st_size <= 0 or before.st_size > _MAX_CACHE_METADATA_BYTES:
        raise PbfDownloadRejected("cached PBF metadata size is invalid")
    try:
        raw = path.read_bytes()
        after = path.lstat()
    except OSError as exc:
        raise PbfDownloadRejected("cached PBF metadata cannot be read") from exc
    if stat.S_ISLNK(after.st_mode) or not stat.S_ISREG(after.st_mode):
        raise PbfDownloadRejected("cached PBF metadata changed during validation")
    if _stat_identity(before) != _stat_identity(after):
        raise PbfDownloadRejected("cached PBF metadata changed during validation")
    try:
        decoded: object = json.loads(raw.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError):
        raise PbfDownloadRejected("cached PBF metadata is invalid JSON") from None
    if not isinstance(decoded, dict):
        raise PbfDownloadRejected("cached PBF metadata must be a JSON object")
    metadata = cast(dict[str, object], decoded)
    if set(metadata) != _CACHE_METADATA_KEYS:
        raise PbfDownloadRejected("cached PBF metadata fields are invalid")

    schema_version = metadata["schema_version"]
    size_bytes = metadata["size_bytes"]
    if (
        isinstance(schema_version, bool)
        or not isinstance(schema_version, int)
        or schema_version != _CACHE_METADATA_SCHEMA_VERSION
    ):
        raise PbfDownloadRejected("cached PBF metadata schema version is invalid")
    if isinstance(size_bytes, bool) or not isinstance(size_bytes, int):
        raise PbfDownloadRejected("cached PBF size metadata is invalid")
    expected_values = {
        "region_id": settings.region_id,
        "source_url": settings.url,
        "content_sha256": settings.expected_sha256,
        "size_bytes": settings.expected_size_bytes,
    }
    for field_name, expected in expected_values.items():
        if metadata[field_name] != expected:
            raise PbfDownloadRejected(f"cached PBF {field_name} metadata does not match")

    downloaded_at = _parse_utc_timestamp(metadata["downloaded_at"])
    etag = _optional_metadata_string(metadata["etag"], "etag")
    last_modified = _optional_metadata_string(metadata["last_modified"], "last_modified")
    return downloaded_at, etag, last_modified


def _content_length(response: object) -> int:
    headers = getattr(response, "headers", None)
    if headers is None:
        raise PbfDownloadRejected("response headers are unavailable")
    raw = headers.get("Content-Length")
    if raw is None:
        raise PbfDownloadRejected("response Content-Length is required")
    try:
        value = int(raw)
    except (TypeError, ValueError):
        raise PbfDownloadRejected("response Content-Length is invalid") from None
    if value <= 0:
        raise PbfDownloadRejected("response Content-Length must be positive")
    return value


def _response_metadata_header(response: object, field_name: str) -> str | None:
    headers = getattr(response, "headers", None)
    if headers is None:
        raise PbfDownloadRejected("response headers are unavailable")
    value = headers.get(field_name)
    if value is None:
        return None
    if not isinstance(value, str):
        raise PbfDownloadRejected(f"response {field_name} is invalid")
    if len(value) > _MAX_HEADER_VALUE_LENGTH or "\r" in value or "\n" in value:
        raise PbfDownloadRejected(f"response {field_name} is invalid")
    return value


def _publish_cache_entry(
    temporary_path: Path,
    target: Path,
    temporary_metadata_path: Path,
    metadata_target: Path,
    downloaded_at: datetime,
) -> None:
    """Publish the metadata and PBF without replacing existing cache files."""

    metadata_published = False
    pbf_published = False
    try:
        os.link(temporary_metadata_path, metadata_target, follow_symlinks=False)
        metadata_published = True
        os.link(temporary_path, target, follow_symlinks=False)
        pbf_published = True
        os.utime(
            target,
            (downloaded_at.timestamp(), downloaded_at.timestamp()),
            follow_symlinks=False,
        )
        temporary_metadata_path.unlink()
        temporary_path.unlink()
    except FileExistsError:
        if pbf_published:
            target.unlink(missing_ok=True)
        if metadata_published:
            metadata_target.unlink(missing_ok=True)
        raise PbfDownloadRejected(
            "content-addressed cache target appeared during download"
        ) from None
    except OSError:
        if pbf_published:
            try:
                target.unlink(missing_ok=True)
            except OSError:
                pass
        if metadata_published:
            try:
                metadata_target.unlink(missing_ok=True)
            except OSError:
                pass
        raise PbfDownloadRejected("content-addressed cache publication failed") from None


def download_approved_test_pbf(
    settings: ApprovedTestPbfDownload,
    cache_dir: Path,
    *,
    clock: Callable[[], datetime] = _utc_now,
) -> ValidatedPbfDownload:
    """Download one exact loopback test URL into a content-addressed cache."""

    if not isinstance(settings, ApprovedTestPbfDownload):
        raise PbfDownloadRejected("settings must be ApprovedTestPbfDownload")
    _validate_cache_directory(cache_dir)
    target = cache_dir / f"{settings.expected_sha256}.osm.pbf"
    metadata_target = _cache_metadata_path(target)
    target_exists = os.path.lexists(target)
    metadata_exists = os.path.lexists(metadata_target)

    if target_exists or metadata_exists:
        if not target_exists or not metadata_exists:
            raise PbfDownloadRejected("content-addressed cache entry is incomplete")
        size = _validate_cached_file(target, settings)
        cache_downloaded_at, cache_etag, cache_last_modified = _read_cache_metadata(
            metadata_target, settings
        )
        validated_at = _clock_value(clock, "validated_at")
        if cache_downloaded_at > validated_at:
            raise PbfDownloadRejected("cached PBF timestamp is in the future")
        return ValidatedPbfDownload(
            region_id=settings.region_id,
            source_url=settings.url,
            local_path=target,
            content_sha256=settings.expected_sha256,
            size_bytes=size,
            downloaded_at=cache_downloaded_at,
            validated_at=validated_at,
            etag=cache_etag,
            last_modified=cache_last_modified,
            cache_hit=True,
        )

    request = urllib.request.Request(
        settings.url,
        headers={
            "Accept": "application/octet-stream",
            "Accept-Encoding": "identity",
            "User-Agent": "osm-lead-source-service-phase-2a3-test/1",
        },
        method="GET",
    )
    temporary_path: Path | None = None
    temporary_metadata_path: Path | None = None
    downloaded_at = _clock_value(clock, "downloaded_at")
    etag: str | None = None
    last_modified: str | None = None

    try:
        with _test_opener().open(request, timeout=settings.timeout_seconds) as response:
            status = getattr(response, "status", response.getcode())
            if status != 200:
                raise PbfDownloadRejected("test PBF response status is not 200")
            if response.geturl() != settings.url:
                raise PbfDownloadRejected("test PBF response URL changed")
            content_encoding = response.headers.get("Content-Encoding")
            if content_encoding not in (None, "", "identity"):
                raise PbfDownloadRejected("compressed HTTP content encoding is not accepted")
            content_type = response.headers.get_content_type()
            if content_type not in _ALLOWED_CONTENT_TYPES:
                raise PbfDownloadRejected("test PBF response content type is not accepted")
            length = _content_length(response)
            if length != settings.expected_size_bytes:
                raise PbfDownloadRejected("response size does not match approved metadata")
            if length > settings.max_size_bytes:
                raise PbfDownloadRejected("response exceeds approved size limit")

            etag = _response_metadata_header(response, "ETag")
            last_modified = _response_metadata_header(response, "Last-Modified")
            digest = hashlib.sha256()
            total = 0
            with tempfile.NamedTemporaryFile(
                mode="wb",
                dir=cache_dir,
                prefix=".pbf-download-",
                suffix=".part",
                delete=False,
            ) as temporary:
                temporary_path = Path(temporary.name)
                while chunk := response.read(_CHUNK_SIZE):
                    total += len(chunk)
                    if total > settings.max_size_bytes:
                        raise PbfDownloadRejected("download exceeds approved size limit")
                    if total > settings.expected_size_bytes:
                        raise PbfDownloadRejected("download exceeds approved expected size")
                    digest.update(chunk)
                    temporary.write(chunk)
                temporary.flush()
                os.fsync(temporary.fileno())

        if total != settings.expected_size_bytes:
            raise PbfDownloadRejected("download size does not match approved metadata")
        if digest.hexdigest() != settings.expected_sha256:
            raise PbfDownloadRejected("download checksum does not match approved metadata")
        if temporary_path is None:
            raise PbfDownloadRejected("temporary download path was not created")

        metadata_bytes = _serialize_cache_metadata(
            settings,
            downloaded_at,
            etag,
            last_modified,
        )
        with tempfile.NamedTemporaryFile(
            mode="wb",
            dir=cache_dir,
            prefix=".pbf-metadata-",
            suffix=".part",
            delete=False,
        ) as temporary_metadata:
            temporary_metadata_path = Path(temporary_metadata.name)
            temporary_metadata.write(metadata_bytes)
            temporary_metadata.flush()
            os.fsync(temporary_metadata.fileno())

        _publish_cache_entry(
            temporary_path,
            target,
            temporary_metadata_path,
            metadata_target,
            downloaded_at,
        )
        temporary_path = None
        temporary_metadata_path = None
        validated_at = _clock_value(clock, "validated_at")
        return ValidatedPbfDownload(
            region_id=settings.region_id,
            source_url=settings.url,
            local_path=target,
            content_sha256=settings.expected_sha256,
            size_bytes=total,
            downloaded_at=downloaded_at,
            validated_at=validated_at,
            etag=etag,
            last_modified=last_modified,
            cache_hit=False,
        )
    except PbfDownloadRejected:
        raise
    except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError):
        raise PbfDownloadRejected("approved test PBF download failed") from None
    finally:
        for temporary_file in (temporary_path, temporary_metadata_path):
            if temporary_file is not None:
                try:
                    temporary_file.unlink(missing_ok=True)
                except OSError:
                    pass
