#!/usr/bin/env python3
"""Rank investors by relevant portfolio evidence for one Dealroom company."""

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

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"
DEFAULT_COMPANY_ID = "a3741a91-cbe8-4a50-9dba-69b4ee612973"
DEFAULT_TAG_IDS = [99301, 2117101, 10016503]
DEFAULT_PORTFOLIO_LOCATION_ID = 34
RETRYABLE_STATUS = {429, 500, 502, 503, 504}


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
    user_agent: 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] | None = None) -> 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": self.user_agent,
                    "Accept": "application/json",
                },
                timeout=30,
            )
            if response.ok:
                payload = response.json()
                tier = (payload.get("page") or {}).get("tier")
                if tier or payload.get("locked"):
                    raise RuntimeError(
                        "The API treated this request as a non-M2M caller. "
                        "Check Authorization and X-Client-Id before using the result."
                    )
                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 image_url(value: str | None) -> str | None:
    if not value:
        return None
    return value if value.startswith(("http://", "https://")) else f"https://{value}"


def compact_money(value: Any) -> str:
    if value is None:
        return "undisclosed"
    number = float(value)
    if number >= 1_000_000_000:
        return f"${number / 1_000_000_000:.1f}B"
    if number >= 1_000_000:
        return f"${number / 1_000_000:.1f}M"
    return f"${number:,.0f}"


def selected_tags(company: dict[str, Any], tag_ids: list[int]) -> list[dict[str, Any]]:
    wanted = set(tag_ids)
    tags = [
        {"id": tag.get("id"), "name": tag.get("name"), "type": tag.get("type")}
        for tag in company.get("tags") or []
        if tag.get("id") in wanted
    ]
    found = {tag["id"] for tag in tags}
    missing = [tag_id for tag_id in tag_ids if tag_id not in found]
    if missing:
        raise ValueError(f"Tag IDs are not attached to the target company: {missing}")
    return tags


def month_label(investments: dict[str, Any]) -> str | None:
    year = investments.get("last_investor_round_year")
    month = investments.get("last_investor_round_month")
    if not year:
        return None
    return f"{int(year):04d}-{int(month):02d}" if month else f"{int(year):04d}"


def overlaps_deal_range(
    deal_sizes: dict[str, Any], target_min: int, target_max: int
) -> bool | None:
    minimum = deal_sizes.get("min")
    maximum = deal_sizes.get("max")
    if minimum is None or maximum is None:
        return None
    return minimum <= target_max and maximum >= target_min


def normalize_investor(
    investor: dict[str, Any], rank: int, target_min: int, target_max: int
) -> dict[str, Any]:
    portfolio = investor.get("portfolio") or {}
    investments = investor.get("investments") or {}
    deal_sizes = investor.get("deal_sizes") or {}
    match_count = portfolio.get("match_count")
    recent = month_label(investments)
    deal_overlap = overlaps_deal_range(deal_sizes, target_min, target_max)

    reasons = []
    if match_count is not None:
        reasons.append(
            f"{int(match_count)} matching portfolio investment"
            f"{'s' if int(match_count) != 1 else ''}"
        )
    if deal_overlap:
        reasons.append("Typical deal range overlaps the target raise")
    if recent:
        reasons.append(f"Last recorded investment in {recent}")

    return {
        "rank": rank,
        "uuid": investor.get("uuid"),
        "name": investor.get("name"),
        "tagline": investor.get("tagline"),
        "image": image_url(investor.get("image")),
        "dealroom_url": investor.get("dealroom_url"),
        "website": investor.get("website"),
        "hq_city": investor.get("hq_city"),
        "hq_country": investor.get("hq_country"),
        "investor_rank": investor.get("investor_rank"),
        "investor_types": investor.get("investor_types") or [],
        "investor_stages": investor.get("investor_stages") or [],
        "deal_sizes_usd": {
            "min": deal_sizes.get("min"),
            "max": deal_sizes.get("max"),
            "overlaps_target": deal_overlap,
        },
        "investments": {
            "total_count": investments.get("total_count"),
            "total_invested_usd": investments.get("total_invested"),
            "preferred_round": investments.get("preferred_round"),
            "last_recorded_date": recent,
        },
        "portfolio": {
            "companies": portfolio.get("companies"),
            "match_count": match_count,
            "match_invested_usd": portfolio.get("match_invested"),
            "top_companies": portfolio.get("top_companies") or [],
        },
        "fit_reasons": reasons,
    }


def build_shortlist(
    client: DealroomClient,
    company_id: str,
    tag_ids: list[int],
    portfolio_location_id: int,
    target_min: int,
    target_max: int,
    active_since: int,
    limit: int,
) -> dict[str, Any]:
    company = client.get(f"/data/entities/{company_id}", {"currency": "USD"})["data"]
    focus_tags = selected_tags(company, tag_ids)
    investor_filter = (
        f"and(last_investor_round_date[gte]:{active_since},"
        f"min_deal_size[lte]:{target_max},"
        f"max_deal_size[gte]:{target_min})"
    )
    query = {
        "portfolio_count_tag": "|".join(str(tag_id) for tag_id in tag_ids),
        "portfolio_count_location": str(portfolio_location_id),
        "filter": investor_filter,
        "sort": "-portfolio_match_count,investor_rank",
        "limit": limit,
        "include_total": "true",
        "currency": "USD",
    }
    payload = client.get("/data/investors", query)
    evidence_rows = [
        investor
        for investor in rows(payload)
        if int((investor.get("portfolio") or {}).get("match_count") or 0) > 0
    ]
    investors = [
        normalize_investor(investor, rank, target_min, target_max)
        for rank, investor in enumerate(evidence_rows, start=1)
    ]
    if not investors:
        raise RuntimeError("The investor query returned no portfolio matches.")

    return {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "source": "Dealroom API early access",
        "api_endpoint": "/data/investors",
        "currency": payload.get("currency", "USD"),
        "methodology": (
            "Dealroom ranks investors by the number of portfolio companies matching "
            "the selected company tags and portfolio location. Filters then screen for "
            "recent activity and an overlapping typical deal-size range."
        ),
        "target": {
            "uuid": company.get("uuid"),
            "name": company.get("name"),
            "tagline": company.get("tagline"),
            "image": image_url(company.get("image")),
            "dealroom_url": company.get("dealroom_url"),
            "website": company.get("website"),
            "hq_city": company.get("hq_city"),
            "hq_country": company.get("hq_country"),
            "focus_tags": focus_tags,
            "target_raise_usd": {"min": target_min, "max": target_max},
        },
        "query": {
            **query,
            "include_total": True,
        },
        "summary": {
            "investors_returned": len(investors),
            "available_matches": (payload.get("page") or {}).get("total"),
            "with_deal_range_overlap": sum(
                investor["deal_sizes_usd"]["overlaps_target"] is True
                for investor in investors
            ),
            "portfolio_matches_shown": sum(
                int(investor["portfolio"]["match_count"] or 0) for investor in investors
            ),
        },
        "investors": investors,
        "limitations": [
            "Portfolio overlap is evidence of experience, not proof of investment intent.",
            "Typical deal sizes and stage preferences can be incomplete or inferred.",
            "A relevant portfolio can also create a conflict with a direct competitor.",
            "Fund reserves, ownership targets, partner interest, and current deployment pace require direct validation.",
            "This shortlist supports fundraising research and is not an investment recommendation.",
        ],
    }


def markdown(shortlist: dict[str, Any]) -> str:
    target = shortlist["target"]
    lines = [
        f"# Investors with portfolio fit for {target['name']}",
        "",
        shortlist["methodology"],
        "",
    ]
    for investor in shortlist["investors"]:
        location = ", ".join(
            value for value in [investor["hq_city"], investor["hq_country"]] if value
        ) or "Location unavailable"
        deal_sizes = investor["deal_sizes_usd"]
        deal_range = (
            f"{compact_money(deal_sizes['min'])} to {compact_money(deal_sizes['max'])}"
            if deal_sizes["min"] is not None and deal_sizes["max"] is not None
            else "Undisclosed"
        )
        lines.extend(
            [
                f"## {investor['rank']}. {investor['name']}",
                investor.get("tagline") or "No tagline available.",
                f"- Headquarters: {location}",
                f"- Matching portfolio investments: {investor['portfolio']['match_count'] or 0}",
                f"- Typical deal range: {deal_range}",
                f"- Last recorded investment: {investor['investments']['last_recorded_date'] or 'Unavailable'}",
                f"- Dealroom: {investor.get('dealroom_url') or 'Not available'}",
                "",
            ]
        )
    return "\n".join(lines)


def parse_args() -> argparse.Namespace:
    current_year = datetime.now(timezone.utc).year
    parser = argparse.ArgumentParser(
        description="Rank investors by relevant Dealroom portfolio evidence."
    )
    parser.add_argument("--company", default=DEFAULT_COMPANY_ID, help="Dealroom company UUID")
    parser.add_argument(
        "--tag",
        type=int,
        action="append",
        dest="tag_ids",
        help="Focus tag ID attached to the target company. Repeat for multiple tags.",
    )
    parser.add_argument(
        "--portfolio-location",
        type=int,
        default=DEFAULT_PORTFOLIO_LOCATION_ID,
        help="Location taxonomy ID used for portfolio match counting",
    )
    parser.add_argument("--target-min", type=int, default=5_000_000)
    parser.add_argument("--target-max", type=int, default=25_000_000)
    parser.add_argument("--active-since", type=int, default=current_year - 2)
    parser.add_argument("--limit", type=int, default=12)
    parser.add_argument("--json", action="store_true", help="Print structured JSON")
    return parser.parse_args()


def main() -> None:
    load_dotenv()
    args = parse_args()
    if not 1 <= args.limit <= 500:
        raise SystemExit("--limit must be between 1 and 500")
    if args.target_min < 0 or args.target_max <= args.target_min:
        raise SystemExit("Target raise bounds must be positive and max must exceed min")

    client_id = os.environ.get("DEALROOM_CLIENT_ID")
    client_secret = os.environ.get("DEALROOM_CLIENT_SECRET")
    if not client_id or not client_secret:
        raise SystemExit("Set DEALROOM_CLIENT_ID and DEALROOM_CLIENT_SECRET in .env")

    client = DealroomClient(
        client_id=client_id,
        client_secret=client_secret,
        user_agent=os.environ.get("DEALROOM_USER_AGENT", "your-investor-fit-workflow/1.0"),
    )
    shortlist = build_shortlist(
        client=client,
        company_id=args.company,
        tag_ids=args.tag_ids or DEFAULT_TAG_IDS,
        portfolio_location_id=args.portfolio_location,
        target_min=args.target_min,
        target_max=args.target_max,
        active_since=args.active_since,
        limit=args.limit,
    )
    print(json.dumps(shortlist, indent=2) if args.json else markdown(shortlist))


if __name__ == "__main__":
    main()
