"""App URL resolution - dynamic base URL per request for multi-client support.

Security: Origin/Referer are browser-set and trusted. Client-provided URLs
(X-CLIENT-URL, X-CLIENT-DOMAIN, body) are NEVER trusted for link generation.
X-CLIENT-* headers are only used to verify consistency with Origin.
"""
from __future__ import annotations
from urllib.parse import urlparse

import structlog
from fastapi import Request

from src.config import get_settings

logger = structlog.get_logger()

# Client headers (optional, for verification only - must match Origin)
HEADER_CLIENT_URL = "X-CLIENT-URL"
HEADER_CLIENT_DOMAIN = "X-CLIENT-DOMAIN"
HEADER_CLIENT_ID = "X-CLIENT-ID"


def _parse_whitelist(whitelist_str: str) -> list[tuple[str, str]]:
    """Parse comma-separated whitelist. Returns list of (origin, full_url) for matching."""
    if not whitelist_str:
        return []
    result = []
    for s in whitelist_str.split(","):
        u = s.strip().rstrip("/")
        if not u:
            continue
        parsed = urlparse(u)
        if parsed.scheme and parsed.netloc:
            origin = f"{parsed.scheme}://{parsed.netloc}".lower()
            result.append((origin, u.lower()))
    return result


def _effective_client_whitelist(whitelist_str: str, app_url: str) -> list[tuple[str, str]]:
    """Whitelist entries plus ``APP_URL`` origin when a whitelist is configured.

  When ``APP_URL_WHITELIST`` is empty, returns [] so redirect validation stays permissive
  (dev). When set, ``APP_URL`` is always implicitly allowed even if omitted from the list.
    """
    result = _parse_whitelist(whitelist_str)
    if not whitelist_str.strip():
        return result
    app_origin = _origin_from_url(app_url)
    if not app_origin:
        return result
    if any(origin == app_origin for origin, _ in result):
        return result
    normalized = app_url.strip().rstrip("/").lower()
    return [*result, (app_origin, normalized)]


def _origin_from_forwarded_host(request: Request) -> str | None:
    """Build origin from X-Forwarded-* when reverse proxy terminates TLS."""
    settings = get_settings()
    if not getattr(settings, "trust_x_forwarded_host", False):
        return None
    raw_host = (request.headers.get("X-Forwarded-Host") or "").split(",")[0].strip()
    if not raw_host:
        return None
    raw_proto = (request.headers.get("X-Forwarded-Proto") or "https").split(",")[0].strip().lower()
    if raw_proto not in ("http", "https"):
        raw_proto = "https"
    return f"{raw_proto}://{raw_host}".rstrip("/").lower()


def _get_request_origin(request: Request) -> str | None:
    """Extract origin from Origin or Referer (browser-set)."""
    origin = request.headers.get("Origin")
    if origin:
        parsed = urlparse(origin)
        if parsed.scheme and parsed.netloc:
            return f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
    referer = request.headers.get("Referer")
    if referer:
        parsed = urlparse(referer)
        if parsed.scheme and parsed.netloc:
            return f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
    return None


def _origin_from_url(url: str) -> str | None:
    """Extract scheme+host from URL."""
    parsed = urlparse(url.strip().rstrip("/"))
    if parsed.scheme and parsed.netloc:
        return f"{parsed.scheme}://{parsed.netloc}".lower()
    return None


def verify_client_headers_against_origin(request: Request) -> None:
    """
    Verify X-CLIENT-URL / X-CLIENT-DOMAIN match Origin when both present.
    Raises ValidationError if inconsistent (potential impersonation).
    Origin is the trust anchor; client headers must not contradict it.
    """
    from src.core.exceptions import ValidationError

    request_origin = _get_request_origin(request)
    x_url = request.headers.get(HEADER_CLIENT_URL)
    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)

    if not x_url and not x_domain:
        return

    req_parsed = urlparse(request_origin or "")
    if not req_parsed.netloc:
        return

    client_netloc = None
    if x_url:
        p = urlparse(x_url.strip())
        if p.netloc:
            client_netloc = p.netloc.lower()
    elif x_domain:
        d = x_domain.strip().lower()
        if d:
            p = urlparse(f"https://{d}" if not d.startswith(("http://", "https://")) else d)
            client_netloc = p.netloc.lower() if p.netloc else None

    if client_netloc and client_netloc != req_parsed.netloc.lower():
        raise ValidationError(
            "X-CLIENT-URL/X-CLIENT-DOMAIN does not match request Origin"
        )


def resolve_app_base_url(request: Request) -> str:
    """
    Resolve app base URL for links/redirects.
    1. Origin/Referer (browser-set) - preferred
    2. X-CLIENT-URL/X-CLIENT-DOMAIN - fallback when Origin missing, validated against whitelist
    3. settings.app_url - final fallback
    """
    settings = get_settings()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    app_url_fallback = settings.app_url.rstrip("/")

    # Collect raw headers for debug
    headers_in = {
        "Origin": request.headers.get("Origin"),
        "Referer": request.headers.get("Referer"),
        HEADER_CLIENT_URL: request.headers.get(HEADER_CLIENT_URL),
        HEADER_CLIENT_DOMAIN: request.headers.get(HEADER_CLIENT_DOMAIN),
        HEADER_CLIENT_ID: request.headers.get(HEADER_CLIENT_ID),
    }

    verify_client_headers_against_origin(request)

    request_origin = _get_request_origin(request)
    if not request_origin:
        request_origin = _origin_from_forwarded_host(request)

    if not whitelist:
        logger.info(
            "app_url_resolved",
            source="fallback",
            reason="whitelist_empty",
            resolved=app_url_fallback,
            headers_in=headers_in,
            request_origin=request_origin,
        )
        return app_url_fallback

    # Try Origin/Referer first
    if request_origin:
        for allowed_origin, _ in whitelist:
            if request_origin == allowed_origin:
                logger.info(
                    "app_url_resolved",
                    source="origin",
                    resolved=allowed_origin,
                    headers_in=headers_in,
                    request_origin=request_origin,
                    whitelist_count=len(whitelist),
                )
                return allowed_origin

    # Fallback: X-CLIENT-* if whitelisted
    x_url = request.headers.get(HEADER_CLIENT_URL)
    if x_url:
        origin = _origin_from_url(x_url)
        if origin:
            for allowed_origin, _ in whitelist:
                if origin == allowed_origin:
                    logger.info(
                        "app_url_resolved",
                        source="x_client_url",
                        resolved=allowed_origin,
                        headers_in=headers_in,
                        x_client_url=x_url,
                        parsed_origin=origin,
                        whitelist_count=len(whitelist),
                    )
                    return allowed_origin

    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)
    if x_domain:
        d = x_domain.strip()
        url = d if d.startswith(("http://", "https://")) else f"https://{d}"
        origin = _origin_from_url(url)
        if origin:
            for allowed_origin, _ in whitelist:
                if origin == allowed_origin:
                    logger.info(
                        "app_url_resolved",
                        source="x_client_domain",
                        resolved=allowed_origin,
                        headers_in=headers_in,
                        x_client_domain=x_domain,
                        parsed_origin=origin,
                        whitelist_count=len(whitelist),
                    )
                    return allowed_origin

    logger.info(
        "app_url_resolved",
        source="fallback",
        reason="no_match",
        resolved=app_url_fallback,
        headers_in=headers_in,
        request_origin=request_origin,
        whitelist_count=len(whitelist),
    )
    return app_url_fallback


def _url_origin_in_whitelist(url: str, whitelist: list[tuple[str, str]]) -> bool:
    """Check if URL's origin is in the allowed whitelist."""
    origin = _origin_from_url(url)
    if not origin:
        return False
    for allowed_origin, _ in whitelist:
        if origin == allowed_origin:
            return True
    return False


def resolve_checkout_redirect_urls(
    request: Request,
    success_url: str | None = None,
    cancel_url: str | None = None,
    default_success_path: str = "/billing/success",
    default_cancel_path: str = "/billing",
) -> tuple[str, str]:
    """
    Resolve success_url and cancel_url for Stripe checkout.
    When provided, validates against allowed client whitelist.
    When omitted, derives from Origin/X-CLIENT-URL/X-CLIENT-DOMAIN via resolve_app_base_url.
    Returns (success_url, cancel_url).
    """
    from src.core.exceptions import ValidationError

    settings = get_settings()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    base = resolve_app_base_url(request)

    def _validate_or_default(url: str | None, default_path: str) -> str:
        if url:
            # When whitelist configured, validate origin; when empty (dev), allow
            if whitelist and not _url_origin_in_whitelist(url, whitelist):
                raise ValidationError(
                    "Redirect URL origin is not in allowed client whitelist. "
                    "Configure APP_URL_WHITELIST with your frontend origins."
                )
            return url.rstrip("/")
        return f"{base.rstrip('/')}{default_path}"

    return (
        _validate_or_default(success_url, default_success_path),
        _validate_or_default(cancel_url, default_cancel_path),
    )


def resolve_redirect_url(
    request: Request,
    default_path: str = "/",
) -> str:
    """
    Resolve a single redirect URL (e.g. for Stripe customer portal return).
    Uses Origin/X-CLIENT-URL/X-CLIENT-DOMAIN via resolve_app_base_url.
    """
    base = resolve_app_base_url(request)
    return f"{base.rstrip('/')}{default_path}"


def resolve_github_user_mirror_return_url(
    request: Request,
    redirect_base: str | None = None,
) -> str:
    """Full URL where the browser should land after GitHub App install (Setup redirect).

    Priority:
    1. ``redirect_base`` query (optional) — must match ``APP_URL_WHITELIST`` or ``APP_URL`` when configured;
       may be origin only (path from ``GITHUB_USER_APP_FRONTEND_REDIRECT_PATH``) or a full URL.
    2. Origin / Referer / X-Forwarded-* / X-CLIENT-* (same rules as ``resolve_app_base_url``)
       + ``GITHUB_USER_APP_FRONTEND_REDIRECT_PATH``.
    3. ``GITHUB_USER_APP_FRONTEND_REDIRECT_URL`` — if it includes a path, used as-is; else
       origin from that setting + redirect path.
    4. ``APP_URL`` + redirect path.

    Use this for multi-location frontends; env URL is only the fallback.
    """
    from src.core.exceptions import ValidationError

    settings = get_settings()
    path = (getattr(settings, "github_user_app_frontend_redirect_path", None) or "/settings/github-user-callback").strip()
    if not path.startswith("/"):
        path = "/" + path

    fb = (settings.github_user_app_frontend_redirect_url or "").strip()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    app_url_fallback = settings.app_url.rstrip("/")

    if redirect_base and redirect_base.strip():
        url = _normalize_github_mirror_redirect_override(redirect_base.strip(), path)
        if whitelist and not _url_origin_in_whitelist(url, whitelist):
            raise ValidationError(
                "redirect_base origin is not in APP_URL_WHITELIST or APP_URL. "
                "Add your frontend origin or pass a whitelisted URL."
            )
        return url.rstrip("/")

    verify_client_headers_against_origin(request)

    headers_in = {
        "Origin": request.headers.get("Origin"),
        "Referer": request.headers.get("Referer"),
        HEADER_CLIENT_URL: request.headers.get(HEADER_CLIENT_URL),
        HEADER_CLIENT_DOMAIN: request.headers.get(HEADER_CLIENT_DOMAIN),
    }

    request_origin = _get_request_origin(request)
    if not request_origin:
        request_origin = _origin_from_forwarded_host(request)

    def _origin_allowed(origin: str | None) -> bool:
        if not origin:
            return False
        if not whitelist:
            return True
        for allowed_origin, _ in whitelist:
            if origin == allowed_origin:
                return True
        return False

    if request_origin and _origin_allowed(request_origin):
        logger.info(
            "github_mirror_return_url",
            source="origin",
            resolved=f"{request_origin}{path}",
            headers_in=headers_in,
        )
        return f"{request_origin.rstrip('/')}{path}"

    x_url = request.headers.get(HEADER_CLIENT_URL)
    if x_url:
        o = _origin_from_url(x_url)
        if o and _origin_allowed(o):
            logger.info("github_mirror_return_url", source="x_client_url", resolved=f"{o}{path}")
            return f"{o.rstrip('/')}{path}"

    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)
    if x_domain:
        d = x_domain.strip()
        url = d if d.startswith(("http://", "https://")) else f"https://{d}"
        o = _origin_from_url(url)
        if o and _origin_allowed(o):
            logger.info("github_mirror_return_url", source="x_client_domain", resolved=f"{o}{path}")
            return f"{o.rstrip('/')}{path}"

    if not whitelist and request_origin:
        logger.info(
            "github_mirror_return_url",
            source="origin_whitelist_empty",
            resolved=f"{request_origin}{path}",
        )
        return f"{request_origin.rstrip('/')}{path}"

    # Fallback chain: env (full or origin+path) → APP_URL + path
    if fb:
        parsed = urlparse(fb if "://" in fb else f"https://{fb}")
        if parsed.scheme and parsed.netloc and parsed.path not in ("", "/"):
            out = fb.rstrip("/")
            if whitelist and not _url_origin_in_whitelist(out, whitelist):
                logger.warning(
                    "github_mirror_return_url",
                    source="fallback_env_full_url_not_whitelisted",
                    resolved=out,
                )
            logger.info("github_mirror_return_url", source="github_user_app_frontend_redirect_url", resolved=out)
            return out
        origin_fb = f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
        if whitelist and not _origin_allowed(origin_fb):
            logger.info(
                "github_mirror_return_url",
                source="fallback_env_origin_skipped_use_app_url",
                origin_fb=origin_fb,
            )
        else:
            combined = f"{origin_fb}{path}"
            logger.info("github_mirror_return_url", source="github_user_app_frontend_redirect_url_origin", resolved=combined)
            return combined

    out = f"{app_url_fallback}{path}"
    logger.info("github_mirror_return_url", source="app_url_fallback", resolved=out)
    return out


def resolve_gitlab_user_mirror_return_url(
    request: Request,
    redirect_base: str | None = None,
) -> str:
    """Full frontend URL where GitLab OAuth callback should land after API exchange."""
    from src.core.exceptions import ValidationError

    settings = get_settings()
    path = (
        getattr(settings, "gitlab_user_frontend_redirect_path", None)
        or "/settings/gitlab-user-callback"
    ).strip()
    if not path.startswith("/"):
        path = "/" + path

    fb = (settings.gitlab_user_frontend_redirect_url or "").strip()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    app_url_fallback = settings.app_url.rstrip("/")

    if redirect_base and redirect_base.strip():
        url = _normalize_github_mirror_redirect_override(redirect_base.strip(), path)
        if whitelist and not _url_origin_in_whitelist(url, whitelist):
            raise ValidationError(
                "redirect_base origin is not in APP_URL_WHITELIST or APP_URL. "
                "Add your frontend origin or pass a whitelisted URL."
            )
        return url.rstrip("/")

    verify_client_headers_against_origin(request)
    request_origin = _get_request_origin(request) or _origin_from_forwarded_host(request)

    def _origin_allowed(origin: str | None) -> bool:
        if not origin:
            return False
        if not whitelist:
            return True
        return any(origin == allowed_origin for allowed_origin, _ in whitelist)

    if request_origin and _origin_allowed(request_origin):
        return f"{request_origin.rstrip('/')}{path}"

    x_url = request.headers.get(HEADER_CLIENT_URL)
    if x_url:
        o = _origin_from_url(x_url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)
    if x_domain:
        d = x_domain.strip()
        url = d if d.startswith(("http://", "https://")) else f"https://{d}"
        o = _origin_from_url(url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    if fb:
        parsed = urlparse(fb if "://" in fb else f"https://{fb}")
        if parsed.scheme and parsed.netloc and parsed.path not in ("", "/"):
            return fb.rstrip("/")
        origin_fb = f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
        if not whitelist or _origin_allowed(origin_fb):
            return f"{origin_fb}{path}"

    return f"{app_url_fallback}{path}"


def resolve_bitbucket_user_mirror_return_url(
    request: Request,
    redirect_base: str | None = None,
) -> str:
    """Full frontend URL where Bitbucket OAuth callback should land after API exchange."""
    from src.core.exceptions import ValidationError

    settings = get_settings()
    path = (
        getattr(settings, "bitbucket_user_frontend_redirect_path", None)
        or "/settings/bitbucket-user-callback"
    ).strip()
    if not path.startswith("/"):
        path = "/" + path

    fb = (settings.bitbucket_user_frontend_redirect_url or "").strip()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    app_url_fallback = settings.app_url.rstrip("/")

    if redirect_base and redirect_base.strip():
        url = _normalize_github_mirror_redirect_override(redirect_base.strip(), path)
        if whitelist and not _url_origin_in_whitelist(url, whitelist):
            raise ValidationError(
                "redirect_base origin is not in APP_URL_WHITELIST or APP_URL. "
                "Add your frontend origin or pass a whitelisted URL."
            )
        return url.rstrip("/")

    verify_client_headers_against_origin(request)
    request_origin = _get_request_origin(request) or _origin_from_forwarded_host(request)

    def _origin_allowed(origin: str | None) -> bool:
        if not origin:
            return False
        if not whitelist:
            return True
        return any(origin == allowed_origin for allowed_origin, _ in whitelist)

    if request_origin and _origin_allowed(request_origin):
        return f"{request_origin.rstrip('/')}{path}"

    x_url = request.headers.get(HEADER_CLIENT_URL)
    if x_url:
        o = _origin_from_url(x_url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)
    if x_domain:
        d = x_domain.strip()
        url = d if d.startswith(("http://", "https://")) else f"https://{d}"
        o = _origin_from_url(url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    if fb:
        parsed = urlparse(fb if "://" in fb else f"https://{fb}")
        if parsed.scheme and parsed.netloc and parsed.path not in ("", "/"):
            return fb.rstrip("/")
        origin_fb = f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
        if not whitelist or _origin_allowed(origin_fb):
            return f"{origin_fb}{path}"

    return f"{app_url_fallback}{path}"


def resolve_azure_devops_mirror_return_url(
    request: Request,
    redirect_base: str | None = None,
) -> str:
    """Full frontend URL where Azure DevOps OAuth callback should land after API exchange."""
    from src.core.exceptions import ValidationError

    settings = get_settings()
    path = (
        getattr(settings, "azure_devops_frontend_redirect_path", None)
        or "/settings/azure-devops-callback"
    ).strip()
    if not path.startswith("/"):
        path = "/" + path

    fb = (settings.azure_devops_frontend_redirect_url or "").strip()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    app_url_fallback = settings.app_url.rstrip("/")

    if redirect_base and redirect_base.strip():
        url = _normalize_github_mirror_redirect_override(redirect_base.strip(), path)
        if whitelist and not _url_origin_in_whitelist(url, whitelist):
            raise ValidationError(
                "redirect_base origin is not in APP_URL_WHITELIST or APP_URL. "
                "Add your frontend origin or pass a whitelisted URL."
            )
        return url.rstrip("/")

    verify_client_headers_against_origin(request)
    request_origin = _get_request_origin(request) or _origin_from_forwarded_host(request)

    def _origin_allowed(origin: str | None) -> bool:
        if not origin:
            return False
        if not whitelist:
            return True
        return any(origin == allowed_origin for allowed_origin, _ in whitelist)

    if request_origin and _origin_allowed(request_origin):
        return f"{request_origin.rstrip('/')}{path}"

    x_url = request.headers.get(HEADER_CLIENT_URL)
    if x_url:
        o = _origin_from_url(x_url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    x_domain = request.headers.get(HEADER_CLIENT_DOMAIN)
    if x_domain:
        d = x_domain.strip()
        url = d if d.startswith(("http://", "https://")) else f"https://{d}"
        o = _origin_from_url(url)
        if o and _origin_allowed(o):
            return f"{o.rstrip('/')}{path}"

    if fb:
        parsed = urlparse(fb if "://" in fb else f"https://{fb}")
        if parsed.scheme and parsed.netloc and parsed.path not in ("", "/"):
            return fb.rstrip("/")
        origin_fb = f"{parsed.scheme}://{parsed.netloc}".rstrip("/").lower()
        if not whitelist or _origin_allowed(origin_fb):
            return f"{origin_fb}{path}"

    return f"{app_url_fallback}{path}"


def _normalize_github_mirror_redirect_override(redirect_base: str, path: str) -> str:
    """If override is origin-only, append ``path``; if it includes a path, use as-is."""
    raw = redirect_base.strip().rstrip("/")
    parsed = urlparse(raw if "://" in raw else f"https://{raw}")
    if not parsed.scheme or not parsed.netloc:
        from src.core.exceptions import ValidationError

        raise ValidationError("redirect_base must be a valid http(s) URL or host")
    if parsed.path not in ("", "/"):
        return raw
    return f"{parsed.scheme}://{parsed.netloc}".rstrip("/") + path


def resolve_single_redirect_url(
    request: Request,
    url: str | None = None,
    default_path: str = "/billing",
) -> str:
    """
    Resolve a single redirect URL. When url provided, validates against whitelist.
    When omitted, derives from Origin/X-CLIENT-* + default_path.
    """
    from src.core.exceptions import ValidationError

    settings = get_settings()
    whitelist = _effective_client_whitelist(settings.app_url_whitelist, settings.app_url)
    base = resolve_app_base_url(request)

    if url:
        if whitelist and not _url_origin_in_whitelist(url, whitelist):
            raise ValidationError(
                "Redirect URL origin is not in allowed client whitelist. "
                "Configure APP_URL_WHITELIST with your frontend origins."
            )
        return url.rstrip("/")
    return f"{base.rstrip('/')}{default_path}"
