"""Minimal LinkedIn OAuth 2.0 client for one-time profile pulls.

PRD §4.3.5 calls for "syncing profiles with public information from
LinkedIn or other platforms, such as job titles and company information".
LinkedIn's official Sign-In API gives us this with the user's consent
and only the basic ``r_liteprofile`` / ``r_emailaddress`` (legacy) or
``profile email`` (OIDC) scopes — no scraping, no Talent Solutions tier.

The flow:
  1. ``start_oauth(request)`` builds the authorisation URL with a CSRF
     token in ``state`` and redirects.
  2. LinkedIn redirects back to ``complete_oauth(request)`` with a code.
  3. We exchange the code for an access token, fetch the userinfo, and
     write into :class:`AlumniProfile` — only blank fields.
  4. The result is logged to :class:`LinkedInSyncLog`.

No background sync, no token storage. The access token is discarded
after the userinfo call so we cannot keep talking to LinkedIn behind
the user's back.
"""
from __future__ import annotations

import json
import logging
import secrets
import urllib.parse
import urllib.request
from dataclasses import dataclass
from typing import Any

from django.conf import settings
from django.urls import reverse
from django.utils import timezone

logger = logging.getLogger(__name__)


class LinkedInError(Exception):
    """Base class for LinkedIn integration errors."""


class LinkedInConfigError(LinkedInError):
    """Settings are missing — feature is effectively disabled."""


@dataclass(frozen=True)
class LinkedInProfileSnapshot:
    sub: str
    given_name: str
    family_name: str
    email: str
    headline: str
    raw: dict[str, Any]


def _client_id() -> str:
    return getattr(settings, "LINKEDIN_CLIENT_ID", "") or ""


def _client_secret() -> str:
    return getattr(settings, "LINKEDIN_CLIENT_SECRET", "") or ""


def _redirect_url(request) -> str:
    explicit = getattr(settings, "LINKEDIN_OAUTH_REDIRECT_URL", "") or ""
    if explicit:
        return explicit
    return request.build_absolute_uri(reverse("alumni_profiles:linkedin_callback"))


def is_enabled() -> bool:
    return bool(_client_id() and _client_secret())


def build_authorize_url(request) -> tuple[str, str]:
    """Return (auth_url, state). Caller must persist state in the session."""
    if not is_enabled():
        raise LinkedInConfigError("LinkedIn OAuth is not configured (set LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET).")
    state = secrets.token_urlsafe(24)
    params = {
        "response_type": "code",
        "client_id": _client_id(),
        "redirect_uri": _redirect_url(request),
        "state": state,
        # OIDC-style scope set; LinkedIn's "Sign In with LinkedIn using OpenID Connect" product.
        "scope": "openid profile email",
    }
    url = "https://www.linkedin.com/oauth/v2/authorization?" + urllib.parse.urlencode(params)
    return url, state


def _http_post_form(url: str, data: dict[str, str], timeout: int = 15) -> dict[str, Any]:
    body = urllib.parse.urlencode(data).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=body,
        method="POST",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Accept": "application/json",
            "User-Agent": "RUFORUM-IILMP/alumni-linkedin/1.0",
        },
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310
        return json.loads(resp.read().decode("utf-8"))


def _http_get_json(url: str, *, token: str, timeout: int = 15) -> dict[str, Any]:
    req = urllib.request.Request(
        url,
        headers={
            "Authorization": f"Bearer {token}",
            "Accept": "application/json",
            "User-Agent": "RUFORUM-IILMP/alumni-linkedin/1.0",
        },
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310
        return json.loads(resp.read().decode("utf-8"))


def exchange_code_for_token(code: str, *, redirect_uri: str) -> str:
    payload = _http_post_form(
        "https://www.linkedin.com/oauth/v2/accessToken",
        {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": redirect_uri,
            "client_id": _client_id(),
            "client_secret": _client_secret(),
        },
    )
    token = payload.get("access_token")
    if not token:
        raise LinkedInError(f"LinkedIn token exchange returned no access_token: {payload}")
    return token


def fetch_userinfo(token: str) -> LinkedInProfileSnapshot:
    """Call /v2/userinfo (OIDC) and parse the response."""
    payload = _http_get_json("https://api.linkedin.com/v2/userinfo", token=token)
    return LinkedInProfileSnapshot(
        sub=str(payload.get("sub") or ""),
        given_name=str(payload.get("given_name") or ""),
        family_name=str(payload.get("family_name") or ""),
        email=str(payload.get("email") or ""),
        # 'headline' isn't in OIDC userinfo today — left blank intentionally;
        # we keep the field so future LinkedIn responses can populate it.
        headline=str(payload.get("headline") or ""),
        raw=payload,
    )


def apply_snapshot(profile, snapshot: LinkedInProfileSnapshot) -> list[str]:
    """Write LinkedIn data into ``profile`` only when fields are blank.

    Returns the list of field names that were filled.
    """
    filled: list[str] = []
    if snapshot.headline and not profile.headline:
        profile.headline = snapshot.headline
        filled.append("headline")
    # Some LinkedIn integrations expose the user's vanity URL in the raw
    # payload; if so, persist it. Skip silently when absent.
    vanity = snapshot.raw.get("vanityName")
    if vanity and not profile.linkedin_url:
        profile.linkedin_url = f"https://www.linkedin.com/in/{vanity}"
        filled.append("linkedin_url")
    if filled:
        profile.save(update_fields=list(set(filled + ["updated_at"])))
    return filled


def record_sync_attempt(
    profile,
    *,
    snapshot: LinkedInProfileSnapshot | None,
    error: str = "",
    fields_filled: list[str] | None = None,
):
    """Audit log; mirrors :class:`apps.alumni.tracking.models.OrcidSyncLog`."""
    from apps.alumni.profiles.models import LinkedInSyncLog

    log, _ = LinkedInSyncLog.objects.get_or_create(profile=profile)
    log.last_attempt_at = timezone.now()
    if snapshot is not None:
        log.linkedin_subject = snapshot.sub or log.linkedin_subject
        log.last_success_at = timezone.now()
        log.last_error = ""
    if error:
        log.last_error = error[:2000]
    if fields_filled:
        log.fields_filled = fields_filled
    log.save()
    return log
