from __future__ import annotations

from ipaddress import ip_address, ip_network

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.types import ASGIApp

from app.core.settings import get_settings


_CONTEXT_SECRET_HEADER = "X-AuditFirm-Context-Secret"
_TENANT_HEADER = "X-Tenant-Code"
_BRANCH_HEADER = "X-Branch-Code"
_YEAR_HEADER = "X-Year-Code"


def _csv_values(value: str | None) -> list[str]:
    return [item.strip() for item in (value or "").split(",") if item.strip()]


def _safe_env(value: str | None) -> str:
    return (value or "").strip().lower()


def _host_matches_trusted_entry(client_host: str, trusted_entry: str) -> bool:
    """Return True when client_host matches a trusted host/IP/CIDR entry.

    Deliberately does not support '*' wildcard. For Docker/Coolify internal
    networks, use an explicit CIDR such as 172.16.0.0/12.
    """
    client_host = (client_host or "").strip().lower()
    trusted_entry = (trusted_entry or "").strip().lower()
    if not client_host or not trusted_entry:
        return False

    if client_host == trusted_entry:
        return True

    try:
        client_ip = ip_address(client_host)
    except ValueError:
        return False

    try:
        if "/" in trusted_entry:
            return client_ip in ip_network(trusted_entry, strict=False)
        return client_ip == ip_address(trusted_entry)
    except ValueError:
        return False


def _normalise_session_int(value):
    if value in (None, "", 0, "0"):
        return None
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


_TENANT_BOUND_DOMAIN_TYPES = {"audit_firm_domain", "audit_firm_subdomain"}


def _is_trusted_tenant_bound_domain(request: Request) -> bool:
    return (
        bool(getattr(request.state, "domain_resolved", False))
        and bool(getattr(request.state, "domain_is_verified", False))
        and (getattr(request.state, "domain_status", None) or "").strip().lower() == "active"
        and (getattr(request.state, "domain_type", None) or "").strip() in _TENANT_BOUND_DOMAIN_TYPES
        and _normalise_session_int(getattr(request.state, "domain_tenant_id", None)) is not None
    )


class ContextResolveMiddleware(BaseHTTPMiddleware):
    def __init__(self, app: ASGIApp) -> None:
        super().__init__(app)
        self.s = get_settings()

    def _context_headers_are_trusted(self, request: Request) -> bool:
        """Permit context headers only from trusted internal callers.

        Public users must not be able to switch tenant/branch/FY by adding
        X-Tenant-Code, X-Branch-Code or X-Year-Code headers. The production-safe
        default is TRUST_CONTEXT_HEADERS=false.
        """
        if not bool(getattr(self.s, "TRUST_CONTEXT_HEADERS", False)):
            return False

        required_secret = (getattr(self.s, "CONTEXT_HEADER_SECRET", "") or "").strip()
        if required_secret:
            supplied_secret = (request.headers.get(_CONTEXT_SECRET_HEADER) or "").strip()
            if supplied_secret != required_secret:
                return False
        elif _safe_env(getattr(self.s, "ENV", "")) in {"prod", "production"}:
            return False

        client_host = request.client.host if request.client else ""
        trusted_entries = _csv_values(getattr(self.s, "TRUST_CONTEXT_HEADER_HOSTS", ""))
        return any(_host_matches_trusted_entry(client_host, item) for item in trusted_entries)

    async def dispatch(self, request: Request, call_next):
        # Trusted production context priority:
        # 1) Authenticated UI session selected tenant/branch/FY.
        # 2) Domain resolver mapping for pre-login/domain-routed requests.
        # 3) Trusted internal headers only when explicitly enabled with secret/host.
        # 4) Application defaults.
        session = request.scope.get("session") or {}
        trust_headers = self._context_headers_are_trusted(request)

        session_tenant_id = _normalise_session_int(session.get("active_tenant_id") or session.get("tenant_id"))
        session_branch_id = _normalise_session_int(session.get("active_branch_id") or session.get("branch_id"))

        session_tenant_code = (session.get("active_tenant_code") or session.get("tenant_code") or "").strip() or None
        session_branch_code = (session.get("active_branch_code") or session.get("branch_code") or "").strip() or None

        domain_tenant_id = _normalise_session_int(getattr(request.state, "domain_tenant_id", None))
        domain_branch_id = _normalise_session_int(getattr(request.state, "domain_branch_id", None))
        domain_tenant_code = getattr(request.state, "domain_tenant_code", None)
        domain_branch_code = getattr(request.state, "domain_branch_code", None)
        tenant_bound_domain = _is_trusted_tenant_bound_domain(request)

        # On a verified active audit-firm hostname, the domain mapping is the
        # authoritative tenant/branch context. Session selection must never
        # escape to another tenant while the user remains on that hostname.
        if tenant_bound_domain:
            active_tenant_id = domain_tenant_id
            active_branch_id = domain_branch_id if domain_branch_id is not None else session_branch_id
            tenant_code = domain_tenant_code or self.s.DEFAULT_TENANT_CODE
            branch_code = domain_branch_code or session_branch_code or self.s.DEFAULT_BRANCH_CODE
        else:
            active_tenant_id = session_tenant_id
            active_branch_id = session_branch_id
            tenant_code = (
                session_tenant_code
                or domain_tenant_code
                or (request.headers.get(_TENANT_HEADER) if trust_headers else None)
                or self.s.DEFAULT_TENANT_CODE
            )
            branch_code = (
                session_branch_code
                or domain_branch_code
                or (request.headers.get(_BRANCH_HEADER) if trust_headers else None)
                or self.s.DEFAULT_BRANCH_CODE
            )

        year_code = (
            session.get("active_financial_year")
            or (request.headers.get(_YEAR_HEADER) if trust_headers else None)
            or self.s.DEFAULT_YEAR_CODE
        )

        request.state.active_tenant_id = active_tenant_id
        request.state.active_branch_id = active_branch_id
        request.state.tenant_bound_domain = tenant_bound_domain
        request.state.tenant_code = tenant_code
        request.state.branch_code = branch_code
        request.state.year_code = year_code
        request.state.context_headers_trusted = trust_headers
        return await call_next(request)
