from __future__ import annotations

from dataclasses import dataclass
from typing import Mapping


class TrustedContextError(ValueError):
    """Raised when required gateway trusted headers are missing or invalid."""


@dataclass(frozen=True, slots=True)
class TrustedContext:
    """Identity and ACL context injected by api.lucos.com — never from request body."""

    request_id: str
    org_id: str
    user_id: str
    user_email: str | None = None
    org_role: str | None = None
    plan_code: str | None = None
    seat_id: str | None = None
    # Gateway-resolved indexed-repo cap. None means unlimited, but only when
    # max_indexed_repos_present is True — otherwise the gateway simply did not say.
    max_indexed_repos: int | None = None
    max_indexed_repos_present: bool = False
    team_ids: tuple[str, ...] = ()
    # When set, caller is team-scoped and may only access these repo IDs.
    # When None, caller is an org member with org-wide repo access.
    allowed_repo_ids: tuple[str, ...] | None = None

    @property
    def is_org_member(self) -> bool:
        return self.allowed_repo_ids is None

    def can_access_repo(self, repo_id: str) -> bool:
        if self.is_org_member:
            return True
        return repo_id in (self.allowed_repo_ids or ())

    def filter_repo_ids(self, repo_ids: list[str]) -> list[str]:
        if self.is_org_member:
            return repo_ids
        allowed = set(self.allowed_repo_ids or ())
        return [repo_id for repo_id in repo_ids if repo_id in allowed]


def _header(headers: Mapping[str, str], name: str) -> str | None:
    for key, value in headers.items():
        if key.lower() == name.lower():
            stripped = value.strip()
            return stripped or None
    return None


def _split_csv(value: str | None) -> tuple[str, ...]:
    if not value:
        return ()
    return tuple(item.strip() for item in value.split(",") if item.strip())


def _parse_max_indexed_repos(raw: str | None) -> tuple[int | None, bool]:
    """Parse the gateway's repo cap header.

    Returns (cap, header_was_present). "unlimited" and "null" mean no cap. A malformed
    value is treated as absent so the local plan table applies — never as unlimited.
    """
    if raw is None:
        return None, False
    normalised = raw.strip().lower()
    if normalised in {"unlimited", "null", "none", ""}:
        return None, True
    try:
        value = int(normalised)
    except ValueError:
        return None, False
    if value < 0:
        return None, False
    return value, True


def parse_trusted_context(headers: Mapping[str, str]) -> TrustedContext:
    """
    Parse gateway trusted headers.

    Gateway sends X-Repo-Ids only for team-only members. Org members omit it.
    """
    request_id = _header(headers, "x-request-id")
    org_id = _header(headers, "x-org-id")
    user_id = _header(headers, "x-user-id")

    missing = [
        name
        for name, value in (
            ("X-Request-Id", request_id),
            ("X-Org-Id", org_id),
            ("X-User-Id", user_id),
        )
        if not value
    ]
    if missing:
        raise TrustedContextError(
            f"Missing required trusted headers: {', '.join(missing)}"
        )

    repo_ids_header = _header(headers, "x-repo-ids")
    allowed_repo_ids: tuple[str, ...] | None
    if repo_ids_header:
        allowed_repo_ids = _split_csv(repo_ids_header)
    else:
        allowed_repo_ids = None

    max_repos_header = _header(headers, "x-max-indexed-repos")
    max_indexed_repos, max_present = _parse_max_indexed_repos(max_repos_header)

    return TrustedContext(
        request_id=request_id,  # type: ignore[arg-type]
        org_id=org_id,  # type: ignore[arg-type]
        user_id=user_id,  # type: ignore[arg-type]
        user_email=_header(headers, "x-user-email"),
        org_role=_header(headers, "x-org-role"),
        plan_code=_header(headers, "x-plan-code"),
        seat_id=_header(headers, "x-seat-id"),
        max_indexed_repos=max_indexed_repos,
        max_indexed_repos_present=max_present,
        team_ids=_split_csv(_header(headers, "x-team-ids")),
        allowed_repo_ids=allowed_repo_ids,
    )
