"""
kinlyze/scoring.py
────────────────────────────────────────────────────────────────────────────────
Kinlyze ownership scoring algorithm.

Composite ownership score per developer per file:
    score = (commit_score  × 0.40)
          + (exclusivity   × 0.40)   ← no PR data in CLI; weight merged up
          + (criticality   × 0.20)

Bus factor = minimum developers whose removal causes ≥50% knowledge loss.

Trend = compare bus factors from recent 90d window vs prior 90d window.
────────────────────────────────────────────────────────────────────────────────
"""

from collections import defaultdict
from datetime import datetime, timezone
from typing import Callable, Dict, List, Optional, Tuple

from .git import (
    get_repo_info,
    get_tracked_files,
    get_contributors,
    get_module_path,
    is_critical_file,
    load_commits_bulk,
    load_commits_window,
)
from .exceptions import NoDataError

# ── Weights ───────────────────────────────────────────────────────────────────

WEIGHTS = {
    "commit":      0.40,
    "exclusivity": 0.40,
    "criticality": 0.20,
}

RECENCY_THRESHOLDS = [
    (30,  3.0),
    (90,  2.0),
    (180, 1.0),
    (365, 0.5),
]


# ── Individual signal scorers ─────────────────────────────────────────────────

def _days_ago(dt: Optional[datetime]) -> int:
    if dt is None:
        return 999
    now = datetime.now(timezone.utc)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return max((now - dt).days, 0)


def _recency_weight(dt: Optional[datetime]) -> float:
    d = _days_ago(dt)
    for threshold, weight in RECENCY_THRESHOLDS:
        if d <= threshold:
            return weight
    return 0.1


def _commit_score(commits: List[Dict]) -> float:
    """
    Recency-weighted commit score.
    30 recent commits = ~100. Older commits decay rapidly.
    """
    if not commits:
        return 0.0
    weighted = sum(_recency_weight(c.get("date")) for c in commits)
    return min((weighted / 20.0) * 100.0, 100.0)


def _exclusivity_score(dev_commit_count: int, total_commit_count: int) -> float:
    """
    What fraction of this file's commits does this developer own?
    Sole author = 100. One of ten equal contributors = 10.
    """
    if total_commit_count == 0:
        return 0.0
    return (dev_commit_count / total_commit_count) * 100.0


def _criticality_score(file_path: str, total_commits: int) -> float:
    """
    How business-critical is this file?
    Critical path (payments/auth/core) = 60 base + frequency bonus up to 40.
    """
    base = 60.0 if is_critical_file(file_path) else 0.0
    freq = min((total_commits / 50.0) * 40.0, 40.0)
    return min(base + freq, 100.0)


def _ownership_score(c_score: float, e_score: float, crit: float) -> float:
    return (
        c_score * WEIGHTS["commit"]      +
        e_score * WEIGHTS["exclusivity"] +
        crit    * WEIGHTS["criticality"]
    )


# ── Bus factor ────────────────────────────────────────────────────────────────

def _bus_factor(scores: Dict[str, float]) -> Tuple[int, Dict[str, float]]:
    """
    Bus factor = min devs to remove before ≥50% knowledge is lost.
    Returns (bus_factor, {email: percentage_of_knowledge})
    """
    if not scores:
        return 1, {}

    total = sum(scores.values())
    if total == 0.0:
        return 1, {}

    ranked      = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    percentages = {email: (score / total * 100) for email, score in ranked}

    cumulative = 0.0
    bf = 0
    for email, score in ranked:
        cumulative += percentages[email]
        bf += 1
        if cumulative >= 50.0:
            break

    return bf, percentages


def risk_level(bf: int) -> str:
    if bf <= 1: return "critical"
    if bf <= 2: return "high"
    if bf <= 3: return "medium"
    return "low"


def _trend(current_bf: int, prev_bf: Optional[int]) -> str:
    if prev_bf is None:
        return "new"
    if current_bf < prev_bf:
        return "worsening"   # lower BF = worse
    if current_bf > prev_bf:
        return "improving"
    return "stable"


# ── Main analysis pipeline ────────────────────────────────────────────────────

def analyze_repo(
    repo_root: str,
    since_days: int = 365,
    min_commits: int = 2,
    progress_cb: Optional[Callable[[str], None]] = None,
) -> Dict:
    """
    Full Kinlyze MVP analysis.

    Returns a structured dict covering all four MVP features:
      - modules      → Feature 1 (Heat Map) + Feature 2 (Bus Factor)
      - developers   → Feature 3 (Developer Profiles)
      - alerts       → Feature 4 (Risk Alerts)
      - summary      → Dashboard overview
    """

    def _progress(msg: str) -> None:
        if progress_cb:
            progress_cb(msg)

    # ── Repo metadata ─────────────────────────────────────────────────────────
    _progress("Reading repository info...")
    repo_info    = get_repo_info(repo_root)
    contributors = get_contributors(repo_root)
    name_map     = {c["email"]: c["name"] for c in contributors}

    # ── Load full analysis window (ONE git call) ───────────────────────────────
    _progress(f"Loading commit history (last {since_days} days)...")
    file_commits = load_commits_bulk(repo_root, since_days, progress_cb=_progress)

    if not file_commits:
        raise NoDataError(since_days)

    _progress(f"Analyzing {len(file_commits)} files...")

    # ── Per-file ownership scores ──────────────────────────────────────────────
    module_scores:  Dict[str, Dict[str, float]]  = defaultdict(lambda: defaultdict(float))
    module_files:   Dict[str, int]               = defaultdict(int)
    module_commits: Dict[str, int]               = defaultdict(int)
    module_last_active: Dict[str, Optional[datetime]] = {}

    dev_total_score: Dict[str, float] = defaultdict(float)
    grand_total_score = 0.0

    files_analyzed = 0

    for file_path, commits in file_commits.items():
        if not commits:
            continue

        # Group commits by author
        by_author: Dict[str, List[Dict]] = defaultdict(list)
        for c in commits:
            if c.get("email"):
                by_author[c["email"]].append(c)

        if not by_author:
            continue

        # Filter noise: skip files touched by only 1 commit total if min_commits > 1
        total_file_commits = sum(len(v) for v in by_author.values())
        if total_file_commits < min_commits:
            continue

        crit   = _criticality_score(file_path, total_file_commits)
        module = get_module_path(file_path)

        # Last activity on this file
        all_dates = [c["date"] for c in commits if c.get("date")]
        last_active = max(all_dates) if all_dates else None
        if last_active:
            prev = module_last_active.get(module)
            if prev is None or last_active > prev:
                module_last_active[module] = last_active

        module_files[module]   += 1
        module_commits[module] += total_file_commits

        for email, dev_commits in by_author.items():
            # Enrich name from contributor map if available
            if email not in name_map and dev_commits[0].get("name"):
                name_map[email] = dev_commits[0]["name"]

            c_s = _commit_score(dev_commits)
            e_s = _exclusivity_score(len(dev_commits), total_file_commits)
            o_s = _ownership_score(c_s, e_s, crit)

            module_scores[module][email]  += o_s
            dev_total_score[email]        += o_s
            grand_total_score             += o_s

        files_analyzed += 1

    if not module_scores:
        raise NoDataError(since_days)

    # ── Trend: load prior 90-day window for comparison ────────────────────────
    _progress("Computing risk trends...")
    trend_window = 90
    recent_file_commits = load_commits_window(repo_root, since_days=trend_window, until_days=0)
    prior_file_commits  = load_commits_window(repo_root, since_days=trend_window * 2, until_days=trend_window)

    def _quick_bf(file_data: Dict[str, List[Dict]], module: str) -> Optional[int]:
        """Fast bus factor from a subset of commits for trend comparison."""
        m_scores: Dict[str, float] = defaultdict(float)
        for fp, commits in file_data.items():
            if get_module_path(fp) != module:
                continue
            by_a: Dict[str, int] = defaultdict(int)
            for c in commits:
                if c.get("email"):
                    by_a[c["email"]] += 1
            total = sum(by_a.values())
            for email, cnt in by_a.items():
                m_scores[email] += _exclusivity_score(cnt, total)
        if not m_scores:
            return None
        bf, _ = _bus_factor(dict(m_scores))
        return bf

    # ── Build module records ───────────────────────────────────────────────────
    _progress("Building module profiles...")
    risk_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
    modules   = []

    for module_path, dev_scores in module_scores.items():
        bf, pcts = _bus_factor(dict(dev_scores))
        rl       = risk_level(bf)

        ranked   = sorted(dev_scores.items(), key=lambda x: x[1], reverse=True)
        primary_email = ranked[0][0]
        primary_name  = name_map.get(primary_email, _name_from_email(primary_email))
        primary_pct   = round(pcts.get(primary_email, 0.0), 1)

        # Trend vs previous 90-day window
        recent_bf = _quick_bf(recent_file_commits, module_path)
        prior_bf  = _quick_bf(prior_file_commits,  module_path)
        trend_str = _trend(recent_bf or bf, prior_bf)

        # Top 5 contributors for this module
        top_contributors = []
        for email, score in ranked[:5]:
            top_contributors.append({
                "name":  name_map.get(email, _name_from_email(email)),
                "email": email,
                "pct":   round(pcts.get(email, 0.0), 1),
                "score": round(score, 2),
            })

        last_active = module_last_active.get(module_path)

        modules.append({
            "module":        module_path,
            "bus_factor":    bf,
            "risk_level":    rl,
            "primary_owner": primary_name,
            "primary_email": primary_email,
            "primary_pct":   primary_pct,
            "contributors":  top_contributors,
            "file_count":    module_files.get(module_path, 0),
            "commit_count":  module_commits.get(module_path, 0),
            "trend":         trend_str,
            "last_active":   last_active,
        })

    modules.sort(key=lambda m: (risk_rank[m["risk_level"]], -m["primary_pct"]))

    # ── Build developer records ───────────────────────────────────────────────
    _progress("Building developer profiles...")
    developers = []

    for email, total in sorted(dev_total_score.items(), key=lambda x: x[1], reverse=True):
        if grand_total_score == 0:
            knowledge_pct = 0.0
        else:
            knowledge_pct = round((total / grand_total_score) * 100, 1)

        name     = name_map.get(email, _name_from_email(email))
        dev_risk = (
            "critical" if knowledge_pct > 30 else
            "high"     if knowledge_pct > 15 else
            "medium"   if knowledge_pct > 5  else
            "low"
        )

        # Modules where this developer is the primary owner
        owned_modules = [
            m["module"] for m in modules
            if m["contributors"] and m["contributors"][0]["email"] == email
        ]

        # Modules where they are the ONLY contributor (sole owner)
        sole_modules = [
            m["module"] for m in modules
            if len(m["contributors"]) == 1 and m["contributors"][0]["email"] == email
        ]

        # Most recent activity
        dev_dates = []
        for fp, commits in file_commits.items():
            for c in commits:
                if c.get("email") == email and c.get("date"):
                    dev_dates.append(c["date"])
        last_active = max(dev_dates) if dev_dates else None
        days_inactive = _days_ago(last_active)

        developers.append({
            "name":           name,
            "email":          email,
            "knowledge_pct":  knowledge_pct,
            "risk":           dev_risk,
            "owned_modules":  owned_modules[:8],
            "sole_modules":   sole_modules,
            "last_active":    last_active,
            "days_inactive":  days_inactive,
        })

    # ── Build risk alerts ─────────────────────────────────────────────────────
    _progress("Evaluating risk thresholds...")
    alerts = _build_alerts(modules, developers)

    # ── Summary ───────────────────────────────────────────────────────────────
    summary = {"critical": 0, "high": 0, "medium": 0, "low": 0}
    for m in modules:
        summary[m["risk_level"]] += 1

    return {
        "repo":          repo_root,
        "repo_info":     repo_info,
        "since_days":    since_days,
        "files_analyzed": files_analyzed,
        "total_modules": len(modules),
        "modules":       modules,
        "developers":    developers,
        "alerts":        alerts,
        "summary":       summary,
    }


# ── Alert generation (Feature 4) ─────────────────────────────────────────────

def _build_alerts(modules: List[Dict], developers: List[Dict]) -> List[Dict]:
    alerts = []

    for m in modules:
        # Alert: bus factor 1 (single point of failure)
        if m["bus_factor"] == 1:
            alerts.append({
                "severity":    "critical",
                "type":        "bus_factor_1",
                "module":      m["module"],
                "title":       f"Single point of failure — {m['module']}",
                "detail":      (
                    f"{m['primary_owner']} holds {m['primary_pct']}% of knowledge. "
                    f"If they leave, this module has no backup owner."
                ),
                "action":      "Schedule a knowledge transfer or pair programming session immediately.",
            })

        # Alert: owner concentration >80% even with BF > 1
        elif m["primary_pct"] >= 80:
            alerts.append({
                "severity":    "high",
                "type":        "high_concentration",
                "module":      m["module"],
                "title":       f"Knowledge concentration risk — {m['module']}",
                "detail":      (
                    f"{m['primary_owner']} owns {m['primary_pct']}% of this module "
                    f"(bus factor {m['bus_factor']})."
                ),
                "action":      "Distribute knowledge through code reviews and documentation.",
            })

        # Alert: worsening trend
        if m["trend"] == "worsening":
            alerts.append({
                "severity":    "medium",
                "type":        "trend_worsening",
                "module":      m["module"],
                "title":       f"Risk increasing — {m['module']}",
                "detail":      "Knowledge concentration has worsened over the last 90 days.",
                "action":      "Review recent contribution patterns. Are others being blocked from this area?",
            })

    # Alert: developer holds >30% of org knowledge
    for dev in developers:
        if dev["knowledge_pct"] > 30:
            alerts.append({
                "severity":    "critical",
                "type":        "org_knowledge_risk",
                "module":      None,
                "title":       f"Critical org-level risk — {dev['name']}",
                "detail":      (
                    f"{dev['name']} holds {dev['knowledge_pct']}% of total org knowledge "
                    f"across {len(dev['owned_modules'])} modules."
                ),
                "action":      "Begin knowledge transfer plan. Consider bus factor interviews.",
            })
        elif dev["knowledge_pct"] > 15:
            alerts.append({
                "severity":    "high",
                "type":        "developer_concentration",
                "module":      None,
                "title":       f"High developer dependency — {dev['name']}",
                "detail":      (
                    f"{dev['name']} holds {dev['knowledge_pct']}% of org knowledge."
                ),
                "action":      "Encourage cross-team code reviews and documentation.",
            })

    # Sort: critical first
    severity_rank = {"critical": 0, "high": 1, "medium": 2, "low": 3}
    alerts.sort(key=lambda a: severity_rank.get(a["severity"], 3))

    return alerts


# ── Utilities ─────────────────────────────────────────────────────────────────

def _name_from_email(email: str) -> str:
    """'sarah.k@company.com' → 'sarah.k'"""
    return email.split("@")[0] if "@" in email else email
