"""Authentication configuration loader.

Reads ``~/.specify/auth.json`` to determine which hosts receive credentials
and which provider/auth-scheme to use.  No credentials are sent without
an explicit opt-in via this file.
"""

from __future__ import annotations

import json
import os
import stat
from dataclasses import dataclass
from fnmatch import fnmatch
from pathlib import Path
from urllib.parse import urlparse


@dataclass(frozen=True)
class AuthConfigEntry:
    """A single provider entry from ``auth.json``."""

    hosts: tuple[str, ...]
    provider: str
    auth: str
    token: str | None = None
    token_env: str | None = None
    # Azure AD service-principal fields
    tenant_id: str | None = None
    client_id: str | None = None
    client_secret_env: str | None = None


def _default_config_path() -> Path:
    """Return ``~/.specify/auth.json``."""
    return Path.home() / ".specify" / "auth.json"


def _is_valid_host_pattern(pattern: str) -> bool:
    """Return True for safe host patterns: exact hostnames or ``*.suffix`` only.

    Rejects patterns like ``*github.com`` (which would match
    ``github.com.evil.com``) or multi-wildcard forms.  Only these two
    forms are accepted:

    * ``example.com``           — exact hostname
    * ``*.example.com``         — leading ``*.`` wildcard; matches subdomains
      such as ``myorg.example.com`` but not ``example.com`` itself
    """
    if "*" not in pattern:
        return True  # exact hostname — already validated as non-empty
    # Only *.suffix is allowed; no other wildcard positions
    return pattern.startswith("*.") and "*" not in pattern[2:]


def load_auth_config(
    path: Path | None = None,
) -> list[AuthConfigEntry]:
    """Load and validate ``auth.json``, returning configured entries.

    Returns an empty list when the file does not exist — this means
    all HTTP requests will be unauthenticated (opt-in model).

    Raises ``ValueError`` on schema violations.  Callers that want
    misconfigurations to fail fast can allow this exception to
    propagate; higher-level HTTP helpers may instead catch it,
    warn, and continue with unauthenticated requests.
    """
    config_path = path or _default_config_path()

    if not config_path.is_file():
        return []

    # Warn (but don't fail) if the file is world-readable (POSIX only).
    if os.name != "nt":
        try:
            mode = config_path.stat().st_mode
            if mode & (stat.S_IRGRP | stat.S_IROTH):
                import warnings

                warnings.warn(
                    f"{config_path} is readable by group/others. "
                    "Consider restricting with: chmod 600 "
                    f"{config_path}",
                    UserWarning,
                    stacklevel=2,
                )
        except OSError:
            pass  # stat failed — skip permission check

    raw = json.loads(config_path.read_text(encoding="utf-8"))

    if not isinstance(raw, dict):
        raise ValueError(f"auth.json must be a JSON object, got {type(raw).__name__}")

    providers_raw = raw.get("providers")
    if not isinstance(providers_raw, list):
        raise ValueError("auth.json must contain a 'providers' array")

    entries: list[AuthConfigEntry] = []
    for i, entry_raw in enumerate(providers_raw):
        if not isinstance(entry_raw, dict):
            raise ValueError(f"providers[{i}]: must be a JSON object")

        hosts = entry_raw.get("hosts")
        if not isinstance(hosts, list) or not hosts:
            raise ValueError(f"providers[{i}]: 'hosts' must be a non-empty array")
        if not all(isinstance(h, str) and h.strip() for h in hosts):
            raise ValueError(f"providers[{i}]: each host must be a non-empty string")
        # Normalize hosts: strip whitespace and lowercase
        hosts = [h.strip().lower() for h in hosts]
        # Reject dangerous wildcard forms (e.g. *github.com matches github.com.evil.com)
        for h in hosts:
            if not _is_valid_host_pattern(h):
                raise ValueError(
                    f"providers[{i}]: invalid host pattern {h!r}. "
                    "Only exact hostnames or '*.suffix' forms are allowed "
                    "(e.g. 'github.com' or '*.visualstudio.com')."
                )

        provider = entry_raw.get("provider", "")
        if not isinstance(provider, str) or not provider:
            raise ValueError(f"providers[{i}]: 'provider' must be a non-empty string")

        auth = entry_raw.get("auth", "")
        if not isinstance(auth, str) or not auth:
            raise ValueError(f"providers[{i}]: 'auth' must be a non-empty string")

        token = entry_raw.get("token")
        token_env = entry_raw.get("token_env")

        # Validate token/token_env types
        if token is not None and (not isinstance(token, str) or not token.strip()):
            raise ValueError(f"providers[{i}]: 'token' must be a non-empty string")
        if token_env is not None and (not isinstance(token_env, str) or not token_env.strip()):
            raise ValueError(f"providers[{i}]: 'token_env' must be a non-empty string")

        # Validate provider+scheme compatibility
        from . import get_provider as _get_provider
        _prov = _get_provider(provider)
        if _prov is None:
            from . import AUTH_REGISTRY
            raise ValueError(
                f"providers[{i}]: unknown provider {provider!r}; "
                f"registered: {sorted(AUTH_REGISTRY.keys())}"
            )
        if auth not in _prov.supported_auth_schemes:
            raise ValueError(
                f"providers[{i}]: provider {provider!r} does not support "
                f"auth scheme {auth!r}; supported: {list(_prov.supported_auth_schemes)}"
            )

        # Validate token source based on auth scheme
        if auth in ("bearer", "basic-pat"):
            if not token and not token_env:
                raise ValueError(
                    f"providers[{i}]: auth={auth!r} requires 'token' or 'token_env'"
                )
        elif auth == "azure-ad":
            tenant_id = entry_raw.get("tenant_id")
            client_id = entry_raw.get("client_id")
            client_secret_env = entry_raw.get("client_secret_env")
            if not all([tenant_id, client_id, client_secret_env]):
                raise ValueError(
                    f"providers[{i}]: auth='azure-ad' requires "
                    "'tenant_id', 'client_id', and 'client_secret_env'"
                )
            for field_name, field_val in [
                ("tenant_id", tenant_id),
                ("client_id", client_id),
                ("client_secret_env", client_secret_env),
            ]:
                if not isinstance(field_val, str) or not field_val.strip():
                    raise ValueError(
                        f"providers[{i}]: '{field_name}' must be a non-empty string"
                    )
        # azure-cli needs no extra fields

        entries.append(
            AuthConfigEntry(
                hosts=tuple(hosts),
                provider=provider,
                auth=auth,
                token=token,
                token_env=token_env,
                tenant_id=entry_raw.get("tenant_id"),
                client_id=entry_raw.get("client_id"),
                client_secret_env=entry_raw.get("client_secret_env"),
            )
        )

    return entries


def find_entries_for_url(
    url: str, entries: list[AuthConfigEntry]
) -> list[AuthConfigEntry]:
    """Return entries whose ``hosts`` match the hostname of *url*."""
    # A malformed authority (e.g. an unterminated IPv6 bracket "https://[::1")
    # makes urlparse/hostname raise ValueError. Treat that the same as a
    # host-less URL: no entry can match, so return no matches rather than
    # leaking a raw ValueError out of the shared HTTP client (build_request /
    # open_url call this before any URL validation).
    try:
        hostname = (urlparse(url).hostname or "").lower()
    except ValueError:
        return []
    if not hostname:
        return []
    return [
        e
        for e in entries
        if any(
            pattern == hostname or fnmatch(hostname, pattern)
            for pattern in e.hosts
        )
    ]
