"""Desktop SSO client for Lambda-based Microsoft auth flows."""

from __future__ import annotations

import os
import base64
import socket
import threading
import time
import webbrowser
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, HTTPServer
from importlib import resources
from typing import Dict, Optional
from urllib.parse import parse_qs, urlencode, urlparse

from .defaults import DEFAULT_SSO_AUTHORIZE_URL, DEFAULT_SSO_REDIRECT_URI
from .errors import SSOAuthError, SSOTokenNotFoundError
from .models import AuthRequest, TokenResponse


def _flatten_query_params(query: str) -> Dict[str, str]:
    parsed = parse_qs(query, keep_blank_values=True)
    return {k: v[0] for k, v in parsed.items() if v}


def _load_callback_success_html() -> str:
    """Load callback success page from package template."""
    try:
        template = resources.files("sso_typsa").joinpath("templates/callback_success.html").read_text(encoding="utf-8")
        logo_data_uri = _load_logo_data_uri()
        return template.replace("{{LOGO_DATA_URI}}", logo_data_uri)
    except Exception:
        # Safe fallback so callback flow still works even if template is missing.
        return (
            "<html><body><h3>Authentication complete.</h3>"
            "<p>You can close this window and return to the application.</p>"
            "</body></html>"
        )


def _load_logo_data_uri() -> str:
    """Load packaged logo as data URI for callback HTML."""
    try:
        logo_bytes = resources.files("sso_typsa").joinpath("templates/logo-grupo-Typsa.svg").read_bytes()
        encoded = base64.b64encode(logo_bytes).decode("ascii")
        return f"data:image/svg+xml;base64,{encoded}"
    except Exception:
        return ""


@dataclass
class _LocalCallbackResult:
    callback_url: Optional[str] = None
    error: Optional[Exception] = None
    done: bool = False


class TypsaSSOClient:
    """High-level helper for desktop SSO using your Lambda auth endpoint."""

    def __init__(
        self,
        authorize_url: str,
        default_redirect_uri: Optional[str] = None,
    ) -> None:
        self.authorize_url = authorize_url.rstrip("?")
        self.default_redirect_uri = default_redirect_uri

    @staticmethod
    def _first_env(*keys: str) -> Optional[str]:
        for key in keys:
            value = os.getenv(key)
            if value and value.strip():
                return value.strip()
        return None

    @classmethod
    def set_config(
        cls,
        authorize_env_keys: tuple[str, ...] = (
            "SSO_AUTHORIZE_URL",
            "TYPSA_SSO_AUTHORIZE_URL",
        ),
        redirect_env_keys: tuple[str, ...] = (
            "SSO_REDIRECT_URI",
            "TYPSA_SSO_REDIRECT_URI",
            "SSO_CALLBACK_URL",
        ),
        default_redirect_uri: str = DEFAULT_SSO_REDIRECT_URI,
    ) -> "TypsaSSOClient":
        """Build client: authorize URL from env if set, else corporate default in package."""
        authorize_url = cls._first_env(*authorize_env_keys) or DEFAULT_SSO_AUTHORIZE_URL
        if not authorize_url or not authorize_url.strip():
            expected = ", ".join(authorize_env_keys)
            raise SSOAuthError(
                f"Missing authorize URL (env override empty and package default missing). "
                f"Set one of: {expected}"
            )

        redirect_uri = cls._first_env(*redirect_env_keys) or default_redirect_uri
        return cls(
            authorize_url=authorize_url,
            default_redirect_uri=redirect_uri,
        )

    def start_auth(
        self,
        redirect_uri: Optional[str] = None,
        email: Optional[str] = None,
    ) -> AuthRequest:
        """Create auth URL with only redirect_uri and email."""
        selected_redirect_uri = redirect_uri or self.default_redirect_uri
        if not selected_redirect_uri:
            raise SSOAuthError("redirect_uri is required.")
        if not email:
            raise SSOAuthError("email is required.")

        params: Dict[str, str] = {
            "redirect_uri": selected_redirect_uri,
            "email": email,
        }

        auth_url = f"{self.authorize_url}?{urlencode(params)}"
        return AuthRequest(auth_url=auth_url, email=email, redirect_uri=selected_redirect_uri)

    def open_auth_in_browser(self, auth_request: AuthRequest) -> None:
        """Open generated auth URL using default browser."""
        webbrowser.open(auth_request.auth_url, new=2)

    def complete_auth(self, callback_url: str) -> TokenResponse:
        """Parse callback URL and extract token values."""
        parsed = urlparse(callback_url)
        query_params = _flatten_query_params(parsed.query)
        fragment_params = _flatten_query_params(parsed.fragment)

        # Merge both because some providers return tokens in fragment.
        params = {**query_params, **fragment_params}

        if "error" in params:
            description = params.get("error_description", "Authentication failed.")
            raise SSOAuthError(f"{params['error']}: {description}")

        id_token = params.get("id_token")
        refresh_token = params.get("refresh_token")
        access_token = params.get("access_token")
        token_type = params.get("token_type")
        expires_in_raw = params.get("expires_in")
        scope = params.get("scope")

        if not id_token:
            raise SSOTokenNotFoundError("id_token not found in callback URL.")

        expires_in = int(expires_in_raw) if expires_in_raw and expires_in_raw.isdigit() else None

        return TokenResponse(
            id_token=id_token,
            refresh_token=refresh_token,
            access_token=access_token,
            token_type=token_type,
            expires_in=expires_in,
            scope=scope,
            state=params.get("state"),
            raw_params=params,
        )

    def authenticate_with_loopback(
        self,
        redirect_uri: str,
        email: str,
        timeout_seconds: int = 180,
        open_browser: bool = True,
    ) -> TokenResponse:
        """
        Run end-to-end auth with local HTTP callback.

        Use this method when redirect_uri points to localhost/127.0.0.1,
        e.g. http://127.0.0.1:8765/callback.
        """
        parsed_redirect = urlparse(redirect_uri)
        if parsed_redirect.scheme not in ("http", "https"):
            raise SSOAuthError("Loopback flow requires an HTTP/HTTPS redirect_uri.")
        if not parsed_redirect.hostname:
            raise SSOAuthError("redirect_uri must include a valid hostname.")
        if not parsed_redirect.port:
            raise SSOAuthError("redirect_uri must include an explicit port.")

        result = _LocalCallbackResult()
        done_event = threading.Event()

        callback_path = parsed_redirect.path or "/"
        callback_host = parsed_redirect.hostname
        callback_port = parsed_redirect.port

        class CallbackHandler(BaseHTTPRequestHandler):
            def do_GET(self) -> None:  # noqa: N802
                if not self.path.startswith(callback_path):
                    self.send_response(404)
                    self.end_headers()
                    return

                full_url = f"{parsed_redirect.scheme}://{self.headers['Host']}{self.path}"
                result.callback_url = full_url
                result.done = True
                done_event.set()

                self.send_response(200)
                self.send_header("Content-Type", "text/html; charset=utf-8")
                self.end_headers()
                success_html = _load_callback_success_html()
                self.wfile.write(success_html.encode("utf-8"))

            def log_message(self, format: str, *args) -> None:  # noqa: A003
                # Keep callback server quiet in desktop apps.
                return

        try:
            server = HTTPServer((callback_host, callback_port), CallbackHandler)
        except OSError as exc:
            raise SSOAuthError(f"Failed to bind callback server on {callback_host}:{callback_port}") from exc

        auth_request = self.start_auth(
            redirect_uri=redirect_uri,
            email=email,
        )

        def serve_once() -> None:
            try:
                server.timeout = 0.5
                start = time.time()
                while not done_event.is_set():
                    if (time.time() - start) >= timeout_seconds:
                        raise TimeoutError("Timed out waiting for SSO callback.")
                    server.handle_request()
            except Exception as exc:  # pragma: no cover
                result.error = exc
                done_event.set()
            finally:
                server.server_close()

        server_thread = threading.Thread(target=serve_once, daemon=True)
        server_thread.start()

        if open_browser:
            self.open_auth_in_browser(auth_request)

        done_event.wait(timeout=timeout_seconds + 2)
        if result.error is not None:
            raise SSOAuthError(str(result.error))
        if not result.callback_url:
            raise SSOAuthError("No callback received before timeout.")

        return self.complete_auth(result.callback_url)

    def authenticate(
        self,
        email: str,
        redirect_uri: Optional[str] = None,
        callback_host: str = "127.0.0.1",
        callback_path: str = "/callback",
        timeout_seconds: int = 180,
        open_browser: bool = True,
    ) -> TokenResponse:
        """
        Simplest end-to-end login for desktop applications.

        Required input: email.
        Redirect URI behavior:
        - Uses provided redirect_uri if given.
        - Else uses default_redirect_uri if configured.
        - Else auto-generates a localhost redirect URI with a free port.
        """
        if not email:
            raise SSOAuthError("email is required.")

        chosen_redirect_uri = redirect_uri or self.default_redirect_uri
        if not chosen_redirect_uri:
            free_port = _pick_free_port(callback_host)
            normalized_path = callback_path if callback_path.startswith("/") else f"/{callback_path}"
            chosen_redirect_uri = f"http://{callback_host}:{free_port}{normalized_path}"

        return self.authenticate_with_loopback(
            redirect_uri=chosen_redirect_uri,
            email=email,
            timeout_seconds=timeout_seconds,
            open_browser=open_browser,
        )

    def authenticate_strict(
        self,
        email: str,
        redirect_uri: Optional[str] = None,
        callback_host: str = "127.0.0.1",
        callback_path: str = "/callback",
        timeout_seconds: int = 180,
        open_browser: bool = True,
        require_refresh_token: bool = True,
    ) -> TokenResponse:
        """
        Run authentication and validate required tokens.

        - id_token is always required.
        - refresh_token can be required via require_refresh_token.
        """
        tokens = self.authenticate(
            email=email,
            redirect_uri=redirect_uri,
            callback_host=callback_host,
            callback_path=callback_path,
            timeout_seconds=timeout_seconds,
            open_browser=open_browser,
        )

        if not tokens.id_token:
            raise SSOTokenNotFoundError("id_token not found in callback URL.")
        if require_refresh_token and not tokens.refresh_token:
            raise SSOTokenNotFoundError("refresh_token not found in callback URL.")
        return tokens


def _pick_free_port(host: str) -> int:
    """Reserve and return a currently free local TCP port."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind((host, 0))
        sock.listen(1)
        port = sock.getsockname()[1]
    return int(port)
