"""
scrylo/scrapers/github_scraper.py
-----------------------------------
GitHub lead source — no API key required for basic use.

GitHub's REST API returns perfectly structured JSON:
  name, company, email, bio, location, blog (website), followers

No LLM needed. No page parsing. Direct field mapping.

Two search modes:
  1. Unauthenticated — 60 req/hour, good for small scrapes
  2. Token auth      — 5,000 req/hour, for power users
     (optional personal access token — github.com/settings/tokens)

Search strategy:
  GitHub search API: /search/users?q=...
  Query terms that surface founders/CEOs:
    - "founder" in:bio language:... location:...
    - "CEO" in:bio followers:>50
    - company name searches

Why GitHub works well:
  - Technical founders almost always have GitHub accounts
  - Many have work emails public in their profile
  - Bio often states their role and company
  - Genuine signal — low-follower private devs filtered out

Limitations:
  - Skews technical (CTO, dev founders) — less good for pure business CEOs
  - Email only visible if user has set it public
  - No guarantee of current role (bios are self-maintained)
"""

from __future__ import annotations

import logging
import re
import time
from dataclasses import dataclass, field

import httpx

from scrylo.scrapers.finder import FoundLead, _score

log = logging.getLogger("scrylo.github")

_GITHUB_API   = "https://api.github.com"
_TIMEOUT      = httpx.Timeout(connect=8.0, read=15.0, write=5.0, pool=5.0)
_SENIORITY_RE = re.compile(
    r"\b(CEO|CTO|CFO|COO|Founder|Co-Founder|Cofounder|Director|"
    r"VP|Owner|President|MD|Managing Director)\b",
    re.IGNORECASE,
)

# GitHub accounts to skip — bots, orgs, demo accounts
_SKIP_LOGINS = {"ghost", "invalid", "dependabot", "renovate"}

# Big-tech employers — if someone lists these as their company they're an employee,
# not a founder. We skip or ignore the company field in those cases.
_BIG_TECH_COMPANIES = {
    "google", "facebook", "meta", "microsoft", "amazon", "apple", "netflix",
    "twitter", "x", "uber", "airbnb", "stripe", "shopify", "salesforce",
    "oracle", "ibm", "intel", "nvidia", "adobe", "atlassian", "linkedin",
    "bytedance", "tiktok", "snap", "pinterest", "reddit", "discord",
    "vercel", "cloudflare", "hashicorp", "datadog", "twilio", "okta",
}

# Domains that are never real email domains
_BAD_EMAIL_DOMAINS = {
    "github.io", "githubusercontent.com", "github.com",
    "gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
    "example.com", "test.com", "domain.com",
}

# Credential suffixes to strip from names
_CREDENTIAL_RE = re.compile(
    r",?\s*(Ph\.?D\.?|M\.?D\.?|M\.?B\.?A\.?|B\.?Sc\.?|M\.?Sc\.?|"
    r"MBA|PhD|MSc|BSc|CPA|CFA|Esq\.?)\s*$",
    re.IGNORECASE,
)

# Usernames that are clearly not real names (all lowercase, camelCase product names, etc.)
_FAKE_NAME_RE = re.compile(r"^[A-Z][a-z]+\d+$|^\d|^[A-Z]{2,}\d")  # e.g. CodeMaster7000


def _headers(token: str = "") -> dict:
    h = {
        "Accept": "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
    }
    if token:
        h["Authorization"] = f"Bearer {token}"
    return h


def _build_queries(user_query: str) -> list[str]:
    """
    Build GitHub user search queries from natural language input.
    GitHub search syntax: https://docs.github.com/en/search-github/searching-on-github/searching-users
    """
    q = (user_query or "").strip()

    # Extract role
    roles = re.findall(
        r"\b(CEO|CTO|Founder|Co-Founder|Director|VP|Owner|President)\b",
        q, re.IGNORECASE,
    )
    role = roles[0] if roles else "Founder"

    # Extract location hint
    loc_match = re.search(
        r"\b(London|Berlin|Lagos|Paris|Amsterdam|NYC|New York|"
        r"San Francisco|Toronto|Sydney|Dubai|Singapore|Mumbai)\b",
        q, re.IGNORECASE,
    )
    location = loc_match.group(0) if loc_match else ""

    # Strip roles + location → get industry/keyword
    keyword = re.sub(
        r"\b(CEO|CTO|CFO|COO|Founder|Co-Founder|Director|VP|Owner|"
        r"London|Berlin|Lagos|Paris|Amsterdam|NYC|New York|"
        r"San Francisco|Toronto|Sydney|Dubai|Singapore|Mumbai)\b",
        "", q, flags=re.IGNORECASE,
    ).strip()

    queries = []

    # Bio search — most direct signal
    if keyword and location:
        queries.append(f'"{role}" in:bio "{keyword}" location:{location} followers:>10')
        queries.append(f'"{role}" in:bio location:{location} repos:>2')
    elif keyword:
        queries.append(f'"{role}" in:bio "{keyword}" followers:>10')
        queries.append(f'"{keyword}" "{role}" in:bio repos:>2')
    elif location:
        queries.append(f'"{role}" in:bio location:{location} followers:>10')
        queries.append(f'"{role}" in:bio location:{location} repos:>2')
    else:
        queries.append(f'"{role}" in:bio followers:>50')
        queries.append(f'"{role}" in:bio repos:>5')

    return queries


def _extract_title_from_bio(bio: str) -> str:
    """Extract job title from GitHub bio string."""
    if not bio:
        return ""
    m = _SENIORITY_RE.search(bio)
    return (m.group(0) or "").strip() if m else ""


def _clean_name(name: str) -> str:
    """Strip credentials and junk from display names."""
    name = (name or "").strip()
    name = _CREDENTIAL_RE.sub("", name).rstrip(",").strip()
    return name


def _is_real_name(name: str) -> bool:
    """
    True if the string looks like a real person's name.
    Rejects: usernames with numbers, all-caps acronyms, single words that
    look like product names or handles.
    """
    if not name or len((name or "").strip()) < 4:
        return False
    # Must have at least two words (first + last)
    parts = (name or "").strip().split()
    if len(parts) < 2:
        return False
    # Each part should start with a capital and contain mostly letters
    for part in parts:
        if not re.match(r"^[A-ZÀ-Ö][a-zA-ZÀ-öø-ÿ\-']{1,}$", part):
            return False
    # Reject obvious usernames — contain digits or look like product names
    if _FAKE_NAME_RE.search(name):
        return False
    return True


def _extract_company_from_bio(bio: str, gh_company: str) -> str:
    """
    Get company name. Prefers bio-extracted over raw GitHub company field
    when the raw value is a big-tech employer (meaning the person is likely
    an employee there, not a founder of that company).
    """
    raw = (gh_company or "").lstrip("@").strip()

    # If company is a big-tech name, look in bio for their *own* startup
    if raw.lower() in _BIG_TECH_COMPANIES:
        m = re.search(r"\bat\s+([A-Z][A-Za-z0-9\s\-&\.]{2,30})", bio or "")
        if m:
            return (m.group(1) or "").strip()
        # Can't determine their startup — return empty rather than mislead
        return ""

    if raw:
        return raw

    # Try "at CompanyName" pattern in bio
    m = re.search(r"\bat\s+([A-Z][A-Za-z0-9\s\-&\.]{2,30})", bio or "")
    return (m.group(1) or "").strip() if m else ""


def _user_to_lead(user: dict) -> FoundLead | None:
    """Map a GitHub user API response to FoundLead."""
    login = user.get("login", "")
    if not login or login.lower() in _SKIP_LOGINS:
        return None

    name    = _clean_name(user.get("name") or "")
    bio     = (user.get("bio") or "").strip()
    company = _extract_company_from_bio(bio, user.get("company"))
    title   = _extract_title_from_bio(bio)
    email   = (user.get("email") or "").strip().lower()
    blog    = (user.get("blog") or "").strip()
    loc     = (user.get("location") or "").strip()

    # If no display name, try to infer from login slug
    if not name:
        name = login.replace("-", " ").replace("_", " ").title()

    # Reject usernames masquerading as names (CodeMaster7000, etc.)
    if not _is_real_name(name):
        return None

    # Extract domain from blog
    domain = ""
    if blog:
        raw_domain = re.sub(r"^https?://(www\.)?", "", blog).split("/")[0].lower()
        if not any(bad in raw_domain for bad in _BAD_EMAIL_DOMAINS):
            domain = raw_domain

    lead = FoundLead(
        name         = name,
        title        = title,
        company      = company,
        domain       = domain,
        email        = email,
        email_confidence = 0.95 if email else None,
        email_verified   = True if email else None,
        linkedin_url = "",
        github_url   = f"https://github.com/{login}",
        location     = loc,
        source_url   = f"https://github.com/{login}",
    )
    lead.score = _score(lead)
    return lead if lead.is_valid() else None


def search_github(
    user_query: str,
    limit:      int = 40,
    token:      str = "",
) -> list[FoundLead]:
    """
    Search GitHub for founders/CEOs matching the query.
    Returns list of FoundLead objects.

    Args:
        user_query: Natural language e.g. "SaaS founder London"
        limit:      Max leads to return
        token:      Optional GitHub personal access token (higher rate limit)
    """
    queries = _build_queries(user_query)
    leads:     list[FoundLead] = []
    seen_names: set[str]       = set()

    with httpx.Client(timeout=_TIMEOUT, headers=_headers(token)) as client:
        for query in queries:
            if len(leads) >= limit:
                break

            try:
                resp = client.get(
                    f"{_GITHUB_API}/search/users",
                    params={"q": query, "per_page": min(30, limit), "sort": "followers"},
                )
            except Exception as e:
                log.warning("GitHub search error: %s", e)
                continue

            if resp.status_code == 403:
                log.warning("GitHub rate limited — add a token for higher limits")
                break
            if resp.status_code == 422:
                log.debug("GitHub invalid query: %s", query)
                continue
            if resp.status_code != 200:
                log.warning("GitHub HTTP %d", resp.status_code)
                continue

            data  = resp.json()
            items = data.get("items", [])
            log.debug("GitHub '%s' → %d results", query[:60], len(items))

            # Fetch full profile for each result (search returns minimal data)
            for item in items:
                if len(leads) >= limit:
                    break

                login = item.get("login", "")
                if not login:
                    continue

                # Rate limit — unauthenticated: 60/hour, authenticated: 5000/hour
                time.sleep(0.1 if token else 0.5)

                try:
                    profile_resp = client.get(f"{_GITHUB_API}/users/{login}")
                    if profile_resp.status_code != 200:
                        continue
                    profile = profile_resp.json()
                except Exception:
                    continue

                lead = _user_to_lead(profile)
                if not lead:
                    continue

                # Deduplicate by name
                name_key = (lead.name or "").lower().strip()
                if name_key in seen_names:
                    continue
                seen_names.add(name_key)

                leads.append(lead)

            # Respect GitHub's search rate limit between queries
            time.sleep(1.0 if token else 3.0)

    log.info("GitHub: %d leads from '%s'", len(leads), user_query)
    return leads[:limit]
