#!/usr/bin/env python3
"""Match a CRM CSV to Dealroom, enrich safe matches, and score the result."""

from __future__ import annotations

import argparse
import csv
import json
import os
import random
import re
import time
import unicodedata
from dataclasses import dataclass
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from pathlib import Path
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}
LEGAL_SUFFIX = re.compile(
    r"\s+(inc|inc\.|llc|l\.l\.c|ltd|ltd\.|limited|corp|corp\.|corporation|co|gmbh|ab|oy|bv|b\.v|nv|sa|sas|srl|spa|pty|plc|kk|pbc)\.?$",
    re.IGNORECASE,
)
COUNTRY_ALIASES = {
    "usa": "united states",
    "us": "united states",
    "u.s.": "united states",
    "uk": "united kingdom",
    "u.k.": "united kingdom",
    "england": "united kingdom",
    "uae": "united arab emirates",
}
SHARED_HOSTS = {
    "notion.so",
    "notion.site",
    "apps.apple.com",
    "play.google.com",
    "github.com",
    "github.io",
    "linkedin.com",
    "facebook.com",
    "medium.com",
    "substack.com",
}
OUTPUT_FIELDS = [
    "account_id",
    "company_name",
    "website",
    "country",
    "owner",
    "match_status",
    "match_confidence",
    "match_method",
    "dealroom_uuid",
    "dealroom_name",
    "dealroom_url",
    "hq_city",
    "hq_country",
    "launch_year",
    "employee_count",
    "employee_growth_1y_pct",
    "is_hiring",
    "open_jobs_count",
    "total_funding_usd",
    "signal_rating",
    "priority_score",
    "suggested_candidate",
    "review_reason",
]


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]) -> 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()
                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 normalize_domain(value: str | None) -> str:
    raw = (value or "").strip()
    if not raw:
        return ""
    parsed = urlparse(raw if "://" in raw else f"https://{raw}")
    host = (parsed.hostname or "").lower().removeprefix("www.").rstrip(".")
    if not host or "." not in host:
        return ""
    if any(host == shared or host.endswith(f".{shared}") for shared in SHARED_HOSTS):
        return ""
    return host


def normalize_name(value: str | None) -> str:
    plain = unicodedata.normalize("NFKD", value or "")
    plain = "".join(character for character in plain if not unicodedata.combining(character))
    plain = LEGAL_SUFFIX.sub("", plain.lower())
    return re.sub(r"[^a-z0-9]+", " ", plain).strip()


def normalize_country(value: str | None) -> str:
    country = re.sub(r"\s+", " ", (value or "").strip().lower())
    return COUNTRY_ALIASES.get(country, country)


def company_candidates(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return [
        item
        for item in results
        if item.get("uuid")
        and (item.get("type") == "company" or item.get("organization_subtype") == "company")
    ]


def pick_match(source: dict[str, str], results: list[dict[str, Any]]) -> dict[str, Any]:
    candidates = company_candidates(results)
    source_domain = normalize_domain(source.get("website"))
    by_domain = [
        item
        for item in candidates
        if source_domain
        and normalize_domain(item.get("website_domain") or item.get("website")) == source_domain
    ]
    if by_domain:
        exact_name = next(
            (
                item
                for item in by_domain
                if normalize_name(item.get("name")) == normalize_name(source.get("company_name"))
            ),
            None,
        )
        return {
            "matched": True,
            "candidate": exact_name or by_domain[0],
            "method": "website_domain",
            "confidence": "high",
            "duplicates": len(by_domain),
        }

    source_name = normalize_name(source.get("company_name"))
    source_country = normalize_country(source.get("country"))
    by_name_country = [
        item
        for item in candidates
        if source_name
        and source_country
        and normalize_name(item.get("name")) == source_name
        and normalize_country(item.get("hq_country")) == source_country
    ]
    if len(by_name_country) == 1:
        return {
            "matched": True,
            "candidate": by_name_country[0],
            "method": "name_country",
            "confidence": "medium",
            "duplicates": 1,
        }
    return {"matched": False, "reason": "ambiguous_name_country" if by_name_country else "no_candidate"}


def name_similarity(left: str | None, right: str | None) -> float:
    left_tokens = set(normalize_name(left).split())
    right_tokens = set(normalize_name(right).split())
    if not left_tokens or not right_tokens:
        return 0.0
    return len(left_tokens & right_tokens) / len(left_tokens | right_tokens)


def review_candidate(source: dict[str, str], results: list[dict[str, Any]]) -> dict[str, Any] | None:
    source_country = normalize_country(source.get("country"))
    if not source_country:
        return None
    candidates = sorted(
        company_candidates(results),
        key=lambda item: name_similarity(source.get("company_name"), item.get("name")),
        reverse=True,
    )
    for candidate in candidates:
        if (
            normalize_country(candidate.get("hq_country")) == source_country
            and name_similarity(source.get("company_name"), candidate.get("name")) >= 0.6
        ):
            return candidate
    return None


def priority_score(entity: dict[str, Any]) -> int:
    company = entity.get("company") or {}
    signal = float(company.get("signal_rating") or 0)
    growth = float(entity.get("employee_count_1y_growth") or 0)
    jobs = float(company.get("open_jobs_count") or 0)
    completeness = sum(
        value not in (None, "", 0, [])
        for value in [
            entity.get("website"),
            entity.get("tagline"),
            entity.get("employee_count"),
            company.get("total_funding") or (entity.get("funding_summary") or {}).get("total_funding"),
            entity.get("tags"),
        ]
    )
    score = (
        min(max(signal, 0), 100) * 0.4
        + min(max(growth, 0), 50) * 0.5
        + (15 if company.get("is_hiring") else 0)
        + min(max(jobs, 0), 20) * 0.5
        + completeness * 2
    )
    return round(min(score, 100))


def blank_result(source: dict[str, str], status: str, suggestion: dict[str, Any] | None, reason: str) -> dict[str, Any]:
    result: dict[str, Any] = {field: "" for field in OUTPUT_FIELDS}
    result.update(source)
    result.update(
        {
            "match_status": status,
            "match_confidence": "low" if suggestion else "none",
            "suggested_candidate": (
                f"{suggestion.get('name')} ({suggestion.get('website_domain') or 'no domain'})"
                if suggestion
                else ""
            ),
            "review_reason": reason,
        }
    )
    return result


def enrich_row(client: DealroomClient, source: dict[str, str]) -> dict[str, Any]:
    query = normalize_domain(source.get("website")) or source.get("company_name") or ""
    search = client.get("/data/search", {"q": query, "types": "company", "limit": 8})
    match = pick_match(source, rows(search))
    if not match["matched"]:
        suggestion = review_candidate(source, rows(search))
        return blank_result(
            source,
            "review" if suggestion else "unmatched",
            suggestion,
            "Similar name and same country, but no deterministic match" if suggestion else match["reason"],
        )

    entity = client.get(
        f"/data/entities/{match['candidate']['uuid']}", {"currency": "USD"}
    )["data"]
    company = entity.get("company") or {}
    result: dict[str, Any] = {field: "" for field in OUTPUT_FIELDS}
    result.update(source)
    result.update(
        {
            "match_status": "matched",
            "match_confidence": match["confidence"],
            "match_method": match["method"],
            "dealroom_uuid": entity.get("uuid"),
            "dealroom_name": entity.get("name"),
            "dealroom_url": entity.get("dealroom_url"),
            "hq_city": entity.get("hq_city"),
            "hq_country": entity.get("hq_country"),
            "launch_year": entity.get("launch_year"),
            "employee_count": entity.get("employee_count"),
            "employee_growth_1y_pct": entity.get("employee_count_1y_growth"),
            "is_hiring": bool(company.get("is_hiring")),
            "open_jobs_count": company.get("open_jobs_count") or 0,
            "total_funding_usd": company.get("total_funding") or (entity.get("funding_summary") or {}).get("total_funding"),
            "signal_rating": company.get("signal_rating"),
            "priority_score": priority_score(entity),
            "review_reason": (
                f"{match['duplicates']} records share the input domain"
                if match["duplicates"] > 1
                else ""
            ),
        }
    )
    return result


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Enrich and score a company CSV with Dealroom data.")
    parser.add_argument("--input", type=Path, default=Path("sample_companies.csv"))
    parser.add_argument("--output", type=Path, default=Path("enriched_companies.csv"))
    parser.add_argument("--json", type=Path, help="Optional path for the same rows as JSON")
    return parser.parse_args()


def main() -> None:
    load_dotenv()
    args = parse_args()
    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_secret,
        os.environ.get("DEALROOM_USER_AGENT", "your-crm-enrichment/1.0"),
    )
    with args.input.open(newline="", encoding="utf-8") as handle:
        source_rows = list(csv.DictReader(handle))
    missing = {"company_name", "website", "country"} - set(source_rows[0] if source_rows else {})
    if missing:
        raise SystemExit(f"Input CSV is missing columns: {', '.join(sorted(missing))}")
    enriched = [enrich_row(client, source) for source in source_rows]
    args.output.parent.mkdir(parents=True, exist_ok=True)
    with args.output.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.DictWriter(handle, fieldnames=OUTPUT_FIELDS, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(enriched)
    if args.json:
        args.json.parent.mkdir(parents=True, exist_ok=True)
        args.json.write_text(json.dumps(enriched, indent=2) + "\n", encoding="utf-8")
    matched = sum(row["match_status"] == "matched" for row in enriched)
    review = sum(row["match_status"] == "review" for row in enriched)
    print(f"Wrote {len(enriched)} rows to {args.output}: {matched} matched, {review} need review")


if __name__ == "__main__":
    main()
