#!/usr/bin/env python3
"""Map verified university alumni founders to the startups they built."""

from __future__ import annotations

import argparse
import json
import os
import random
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from typing import Any
from urllib.parse import urlparse

import requests
from dotenv import load_dotenv


API_BASE = "https://api.beta.dealroom.app"
TOKEN_URL = "https://accounts.beta.dealroom.co/oauth/token"
AUDIENCE = "https://api-next.beta.dealroom.co"
RETRYABLE_STATUS = {429, 500, 502, 503, 504}
FOUNDER_BATCH = 50


def retry_delay(value: str | None, fallback: float) -> float:
    if not value:
        return fallback
    try:
        seconds = float(value)
    except ValueError:
        try:
            retry_at = parsedate_to_datetime(value)
            if retry_at.tzinfo is None:
                retry_at = retry_at.replace(tzinfo=timezone.utc)
            seconds = (retry_at - datetime.now(timezone.utc)).total_seconds()
        except (TypeError, ValueError, OverflowError):
            return fallback
    return min(max(seconds, 0.0), 30.0)


@dataclass
class DealroomClient:
    client_id: str
    client_secret: str
    token: str | None = None

    def authenticate(self) -> None:
        response = requests.post(
            TOKEN_URL,
            json={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "audience": AUDIENCE,
            },
            headers={"Accept": "application/json"},
            timeout=30,
        )
        response.raise_for_status()
        self.token = response.json()["access_token"]

    def get(self, path: str, params: dict[str, Any]) -> dict[str, Any]:
        if not self.token:
            self.authenticate()
        last_error: requests.HTTPError | None = None
        for attempt in range(4):
            response = requests.get(
                f"{API_BASE}{path}",
                params=params,
                headers={
                    "Authorization": f"Bearer {self.token}",
                    "X-Client-Id": self.client_id,
                    "User-Agent": "dealroom-university-founder-cookbook/1.0",
                    "Accept": "application/json",
                },
                timeout=30,
            )
            if response.ok:
                payload = response.json()
                if (payload.get("page") or {}).get("tier") or payload.get("locked"):
                    raise RuntimeError("The API treated this as a capped non-M2M request.")
                return payload
            if response.status_code == 401 and attempt == 0:
                self.authenticate()
                continue
            try:
                response.raise_for_status()
            except requests.HTTPError as error:
                last_error = error
            if response.status_code not in RETRYABLE_STATUS or attempt == 3:
                raise last_error or RuntimeError(response.text)
            delay = retry_delay(response.headers.get("Retry-After"), 0.3 * (2**attempt))
            time.sleep(delay + random.uniform(0, 0.2))
        raise last_error or RuntimeError("Dealroom request failed")


def rows(payload: dict[str, Any]) -> list[dict[str, Any]]:
    value = payload.get("data")
    return value if isinstance(value, list) else []


def domain(value: str | None) -> str:
    raw = (value or "").strip()
    if not raw:
        return ""
    parsed = urlparse(raw if "://" in raw else f"https://{raw}")
    return (parsed.hostname or "").lower().removeprefix("www.")


def batches(values: list[str], size: int) -> list[list[str]]:
    return [values[index : index + size] for index in range(0, len(values), size)]


def founder_at_university(founder: dict[str, Any], university_id: str) -> dict[str, Any] | None:
    education = next(
        (item for item in founder.get("education", []) if item.get("university_uuid") == university_id),
        None,
    )
    if not education:
        return None
    jobs = sorted(
        (item for item in founder.get("companies", []) if not item.get("is_founder")),
        key=lambda item: item.get("year_end") or item.get("year_start") or 0,
        reverse=True,
    )
    return {
        "uuid": founder.get("uuid"),
        "name": founder.get("name"),
        "degree": education.get("degree"),
        "majors": education.get("majors") or [],
        "year_start": education.get("year_start"),
        "year_end": education.get("year_end"),
        "prior_employers": list(dict.fromkeys(
            item.get("entity_name") for item in jobs[:3] if item.get("entity_name")
        )),
        "dealroom_url": founder.get("dealroom_url"),
    }


def resolve_university(
    client: DealroomClient, name: str, website_domain: str
) -> dict[str, Any]:
    payload = client.get("/data/search", {"q": name, "types": "university", "limit": 10})
    match = next(
        (
            item
            for item in rows(payload)
            if item.get("name") == name and domain(item.get("website_domain")) == website_domain
        ),
        None,
    )
    if not match:
        raise RuntimeError(f"Could not resolve {name!r} by exact name and domain.")
    return client.get(f"/data/entities/{match['uuid']}", {"currency": "USD"})["data"]


def build_network(
    client: DealroomClient, university_name: str, website_domain: str, pool: int, limit: int
) -> dict[str, Any]:
    if any(character in university_name for character in ",()"):
        raise ValueError("University names containing commas or parentheses need filter escaping.")
    university = resolve_university(client, university_name, website_domain)
    company_filter = (
        f"and(founder_university_name[eq]:{university_name},"
        "has_founder[eq]:true,is_startup[eq]:true)"
    )
    company_payload = client.get(
        "/data/companies",
        {
            "filter": company_filter,
            "sort": "-total_funding",
            "limit": pool,
            "include_total": "true",
            "currency": "USD",
        },
    )
    company_pool = rows(company_payload)
    founder_ids = list(dict.fromkeys(
        founder.get("uuid")
        for company in company_pool
        for founder in company.get("founders", [])
        if founder.get("uuid")
    ))
    founders: dict[str, dict[str, Any]] = {}
    for batch in batches(founder_ids, FOUNDER_BATCH):
        payload = client.get(
            "/data/founders",
            {
                "filter": f"id[in_any]:{'|'.join(batch)}",
                "limit": len(batch),
                "currency": "USD",
            },
        )
        founders.update({founder["uuid"]: founder for founder in rows(payload)})

    companies = []
    for entity in company_pool:
        verified = [
            founder_at_university(founders[stub["uuid"]], university["uuid"])
            for stub in entity.get("founders", [])
            if stub.get("uuid") in founders
        ]
        verified = [founder for founder in verified if founder]
        if not verified:
            continue
        company = entity.get("company") or {}
        companies.append(
            {
                "name": entity.get("name"),
                "hq_country": entity.get("hq_country"),
                "launch_year": entity.get("launch_year"),
                "total_funding_usd": company.get("total_funding")
                or (entity.get("funding_summary") or {}).get("total_funding"),
                "is_spinout": bool(entity.get("is_spinout")),
                "dealroom_url": entity.get("dealroom_url"),
                "founders": verified,
            }
        )
        if len(companies) == limit:
            break

    metrics = university.get("university") or {}
    return {
        "university": {
            "uuid": university.get("uuid"),
            "name": university.get("name"),
            "alumni_founder_count": metrics.get("alumni_founder_count"),
            "alumni_founded_companies_count": metrics.get("alumni_founded_companies_count"),
            "spinout_count": metrics.get("spinout_count"),
        },
        "matching_companies_available": (company_payload.get("page") or {}).get("total"),
        "companies": companies,
        "note": (
            "A company spinout flag does not identify its originating institution. "
            "Do not treat every alumni-founded company as a university spinout."
        ),
    }


def print_markdown(network: dict[str, Any]) -> None:
    university = network["university"]
    print(f"# {university['name']} founder network\n")
    print(
        f"{university.get('alumni_founder_count') or 'Unknown'} founder alumni; "
        f"{university.get('spinout_count') or 'unknown'} university-level spinouts.\n"
    )
    for index, company in enumerate(network["companies"], start=1):
        funding = company.get("total_funding_usd")
        funding_label = f"${funding / 1_000_000:,.1f}M" if funding else "funding undisclosed"
        founder_names = ", ".join(founder["name"] for founder in company["founders"])
        print(f"{index}. **{company['name']}** | {funding_label} | {founder_names}")
    print(f"\nNote: {network['note']}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--university", default="University of Oxford")
    parser.add_argument("--domain", default="ox.ac.uk")
    parser.add_argument("--pool", type=int, default=60, choices=range(12, 201))
    parser.add_argument("--limit", type=int, default=12, choices=range(1, 51))
    parser.add_argument("--json", action="store_true", help="Print JSON instead of Markdown")
    args = parser.parse_args()

    load_dotenv()
    client_id = os.getenv("DEALROOM_CLIENT_ID")
    client_secret = os.getenv("DEALROOM_CLIENT_SECRET")
    if not client_id or not client_secret:
        raise SystemExit("Set DEALROOM_CLIENT_ID and DEALROOM_CLIENT_SECRET in .env.")
    network = build_network(
        DealroomClient(client_id, client_secret),
        args.university,
        domain(args.domain),
        max(args.pool, args.limit),
        args.limit,
    )
    if len(network["companies"]) < args.limit:
        raise SystemExit(
            f"Only {len(network['companies'])} companies had a founder education record "
            "that verified the university UUID. Increase --pool."
        )
    if args.json:
        print(json.dumps(network, indent=2))
    else:
        print_markdown(network)


if __name__ == "__main__":
    main()
