"""Analyze CODEOWNERS coverage, ownership distribution, and unowned files.

Output formats: text (default), ``--json``. SARIF is deliberately NOT
emitted because codeowners outputs are per-file owner attribution
metadata (advisory, not violations) — not per-code-location
violations. See action.yml _SUPPORTED_SARIF allowlist +
W1175-RESEARCH propagation plan + W1197-audit memo.
"""

from __future__ import annotations

from collections import defaultdict

import click

from roam.capability import roam_capability
from roam.commands.codeowners_helpers import (  # noqa: F401 — re-exported for backward compat
    _CODEOWNERS_LOCATIONS,
    _codeowners_match,
    find_codeowners,
    parse_codeowners,
    resolve_owners,
)
from roam.commands.resolve import ensure_index
from roam.db.connection import find_project_root, open_db
from roam.output.formatter import format_table, json_envelope, to_json

# ---------------------------------------------------------------------------
# Analysis helpers
# ---------------------------------------------------------------------------


def _key_areas(file_paths: list[str], max_areas: int = 3) -> list[str]:
    """Extract the most common directory prefixes from a list of file paths."""
    dir_counts: dict[str, int] = defaultdict(int)
    for fp in file_paths:
        fp = fp.replace("\\", "/")
        parts = fp.split("/")
        if len(parts) >= 2:
            # Use first two path components as the area
            area = "/".join(parts[:2]) + "/"
        elif len(parts) == 1:
            area = "./"
        else:
            area = "./"
        dir_counts[area] += 1

    sorted_areas = sorted(dir_counts.items(), key=lambda x: x[1], reverse=True)
    return [area for area, _count in sorted_areas[:max_areas]]


def _get_blame_top_contributor(conn, file_id: int) -> str | None:
    """Get the top contributor for a file from git data.

    Uses stored git_file_changes data (fast) rather than running blame.
    Returns the author name or None if no data.
    """
    row = conn.execute(
        """SELECT gc.author, SUM(gfc.lines_added + gfc.lines_removed) AS churn
           FROM git_file_changes gfc
           JOIN git_commits gc ON gfc.commit_id = gc.id
           WHERE gfc.file_id = ?
           GROUP BY gc.author
           ORDER BY churn DESC
           LIMIT 1""",
        (file_id,),
    ).fetchone()
    if row:
        return row["author"]
    return None


# ---------------------------------------------------------------------------
# Command
# ---------------------------------------------------------------------------


@roam_capability(
    name="codeowners",
    category="reports",
    summary="Analyze CODEOWNERS coverage and ownership distribution",
    maturity="stable",
    mcp_expose=True,
    mcp_preset=("core",),
    side_effect=False,
    task_required=False,
    destructive=False,
    stale_sensitive=True,
    ai_safe=True,
    requires_index=True,
)
@click.command("codeowners")
@click.option("--unowned", is_flag=True, help="Show only unowned files")
@click.option("--owner", default="", help="Filter by specific owner")
@click.option("--limit", default=30, help="Max items to display")
@click.pass_context
def codeowners(ctx, unowned, owner, limit):
    """Analyze CODEOWNERS coverage and ownership distribution.

    Unlike ``drift`` (which measures time-decayed ownership divergence) and
    ``suggest-reviewers`` (which recommends PR reviewers from commit history),
    this command analyzes static CODEOWNERS coverage, ownership distribution,
    and unowned files.
    """
    json_mode = ctx.obj.get("json") if ctx.obj else False
    budget = ctx.obj.get("budget", 0) if ctx.obj else 0
    ensure_index()

    project_root = find_project_root()
    co_path = find_codeowners(project_root)

    # --- No CODEOWNERS file ---
    if co_path is None:
        searched = ", ".join(_CODEOWNERS_LOCATIONS)
        if json_mode:
            click.echo(
                to_json(
                    json_envelope(
                        "codeowners",
                        budget=budget,
                        summary={
                            "verdict": "No CODEOWNERS file found",
                            "codeowners_found": False,
                            "searched": _CODEOWNERS_LOCATIONS,
                        },
                    )
                )
            )
            return
        click.echo("VERDICT: No CODEOWNERS file found")
        click.echo()
        click.echo(f"  Searched: {searched}")
        click.echo()
        click.echo("  Consider creating a CODEOWNERS file to define code ownership.")
        click.echo("  Run `roam codeowners --unowned` after creating it to find coverage gaps.")
        return

    rules = parse_codeowners(co_path)
    co_relpath = str(co_path.relative_to(project_root)).replace("\\", "/")

    with open_db(readonly=True) as conn:
        # Get all indexed files
        all_files = conn.execute("SELECT id, path FROM files ORDER BY path").fetchall()

        if not all_files:
            if json_mode:
                click.echo(
                    to_json(
                        json_envelope(
                            "codeowners",
                            budget=budget,
                            summary={
                                "verdict": "No files in index",
                                "total_files": 0,
                            },
                        )
                    )
                )
            else:
                click.echo("VERDICT: No files in index")
            return

        # Resolve ownership for each file
        file_ownership: list[dict] = []
        for f in all_files:
            fpath = f["path"].replace("\\", "/")
            owners = resolve_owners(rules, fpath)
            file_ownership.append(
                {
                    "file_id": f["id"],
                    "path": fpath,
                    "owners": owners,
                }
            )

        total_files = len(file_ownership)
        owned_files = [fo for fo in file_ownership if fo["owners"]]
        unowned_files = [fo for fo in file_ownership if not fo["owners"]]
        owned_count = len(owned_files)
        unowned_count = len(unowned_files)
        coverage_pct = round(owned_count * 100 / total_files, 1) if total_files else 0

        # Build owner -> files mapping
        owner_files: dict[str, list[str]] = defaultdict(list)
        for fo in owned_files:
            for o in fo["owners"]:
                owner_files[o].append(fo["path"])

        # PageRank for unowned files (for importance ranking)
        unowned_ids = [fo["file_id"] for fo in unowned_files]
        pagerank_map: dict[int, float] = {}
        dependents_map: dict[int, int] = {}
        if unowned_ids:
            # Get PageRank from graph_metrics via symbols
            for fo in unowned_files:
                fid = fo["file_id"]
                pr_row = conn.execute(
                    """SELECT MAX(gm.pagerank) AS pr
                       FROM graph_metrics gm
                       JOIN symbols s ON s.id = gm.symbol_id
                       WHERE s.file_id = ?""",
                    (fid,),
                ).fetchone()
                pagerank_map[fid] = (pr_row["pr"] or 0.0) if pr_row else 0.0

                dep_row = conn.execute(
                    """SELECT COUNT(DISTINCT e.source_id) AS cnt
                       FROM edges e
                       JOIN symbols s ON s.id = e.target_id
                       WHERE s.file_id = ?""",
                    (fid,),
                ).fetchone()
                dependents_map[fid] = dep_row["cnt"] if dep_row else 0

        # Sort unowned by PageRank descending
        unowned_ranked = sorted(
            unowned_files,
            key=lambda fo: pagerank_map.get(fo["file_id"], 0),
            reverse=True,
        )

        # Build owner distribution table
        owner_dist = []
        for oname in sorted(owner_files.keys(), key=lambda o: len(owner_files[o]), reverse=True):
            files = owner_files[oname]
            areas = _key_areas(files)
            owner_dist.append(
                {
                    "name": oname,
                    "files": len(files),
                    "pct": round(len(files) * 100 / total_files, 1),
                    "key_areas": areas,
                }
            )

        # Ownership drift: compare declared owner vs top git contributor
        drift_files: list[dict] = []
        for fo in owned_files:
            top_contributor = _get_blame_top_contributor(conn, fo["file_id"])
            if top_contributor and fo["owners"]:
                # Check if top contributor is NOT one of the declared owners
                # Normalize: owners might have @ prefix, contributors might not
                declared_names = {o.lstrip("@").lower() for o in fo["owners"]}
                contributor_name = top_contributor.lower()
                if contributor_name not in declared_names:
                    drift_files.append(
                        {
                            "path": fo["path"],
                            "declared_owners": fo["owners"],
                            "top_contributor": top_contributor,
                        }
                    )

        # Build verdict
        verdict = f"{coverage_pct}% CODEOWNERS coverage ({owned_count}/{total_files} files owned)"

        # --- Filter modes ---

        if owner:
            # Filter by specific owner
            owner_matched = owner_files.get(owner, [])
            if not owner_matched:
                # Try case-insensitive and with/without @ prefix
                for oname, ofiles in owner_files.items():
                    if oname.lower() == owner.lower() or oname.lstrip("@").lower() == owner.lstrip("@").lower():
                        owner_matched = ofiles
                        owner = oname
                        break

            if json_mode:
                owner_file_details = []
                for fp in owner_matched[:limit]:
                    frow = conn.execute("SELECT id FROM files WHERE path = ?", (fp,)).fetchone()
                    pr = 0.0
                    deps = 0
                    if frow:
                        pr_row = conn.execute(
                            """SELECT MAX(gm.pagerank) AS pr
                               FROM graph_metrics gm
                               JOIN symbols s ON s.id = gm.symbol_id
                               WHERE s.file_id = ?""",
                            (frow["id"],),
                        ).fetchone()
                        pr = (pr_row["pr"] or 0.0) if pr_row else 0.0
                        dep_row = conn.execute(
                            """SELECT COUNT(DISTINCT e.source_id) AS cnt
                               FROM edges e
                               JOIN symbols s ON s.id = e.target_id
                               WHERE s.file_id = ?""",
                            (frow["id"],),
                        ).fetchone()
                        deps = dep_row["cnt"] if dep_row else 0
                    # W-dogfood (W336 sibling): 6-decimal rounding.
                    # `pr` is MAX symbol-level PR for the file (see
                    # line ~278). On 5K+ symbol graphs the symbol-PR
                    # floor is ~1.4e-05, so 4-decimal rounding collapses
                    # ~72% of nonzero values to 0.0. Matches cmd_search
                    # / cmd_intent / cmd_hover precedent.
                    owner_file_details.append({"path": fp, "pagerank": round(pr, 6), "dependents": deps})

                click.echo(
                    to_json(
                        json_envelope(
                            "codeowners",
                            budget=budget,
                            summary={
                                "verdict": f"{owner}: {len(owner_matched)} files",
                                "owner": owner,
                                "file_count": len(owner_matched),
                            },
                            files=owner_file_details,
                        )
                    )
                )
                return

            click.echo(f"VERDICT: {owner}: {len(owner_matched)} files")
            click.echo()
            if not owner_matched:
                click.echo(f"  No files found for owner: {owner}")
                return
            rows = []
            for fp in owner_matched[:limit]:
                rows.append([fp])
            click.echo(format_table(["File"], rows))
            if len(owner_matched) > limit:
                click.echo(f"  (+{len(owner_matched) - limit} more)")
            return

        if unowned:
            # Show only unowned files
            if json_mode:
                items = [
                    {
                        "path": fo["path"],
                        # W-dogfood (W336 sibling): 6-decimal precision —
                        # 4-decimal collapsed ~72% of nonzero PR values to
                        # 0.0 on 5K+ symbol graphs.
                        "pagerank": round(pagerank_map.get(fo["file_id"], 0), 6),
                        "dependents": dependents_map.get(fo["file_id"], 0),
                    }
                    for fo in unowned_ranked[:limit]
                ]
                click.echo(
                    to_json(
                        json_envelope(
                            "codeowners",
                            budget=budget,
                            summary={
                                "verdict": f"{unowned_count} unowned files ({100 - coverage_pct}% of codebase)",
                                "unowned_count": unowned_count,
                                "total_files": total_files,
                            },
                            unowned=items,
                        )
                    )
                )
                return

            click.echo(f"VERDICT: {unowned_count} unowned files ({100 - coverage_pct}% of codebase)")
            click.echo()
            if not unowned_ranked:
                click.echo("  All files have declared owners.")
                return
            click.echo("  Unowned Files (by PageRank):")
            rows = []
            for fo in unowned_ranked[:limit]:
                pr = pagerank_map.get(fo["file_id"], 0)
                deps = dependents_map.get(fo["file_id"], 0)
                rows.append([fo["path"], f"{pr:.4f}", str(deps)])
            click.echo(format_table(["File", "PageRank", "Dependents"], rows))
            if len(unowned_ranked) > limit:
                click.echo(f"  (+{len(unowned_ranked) - limit} more)")
            return

        # --- Full report ---
        if json_mode:
            unowned_items = [
                {
                    "path": fo["path"],
                    # W-dogfood (W336 sibling): 6-decimal precision —
                    # 4-decimal collapsed ~72% of nonzero PR values to
                    # 0.0 on 5K+ symbol graphs.
                    "pagerank": round(pagerank_map.get(fo["file_id"], 0), 6),
                    "dependents": dependents_map.get(fo["file_id"], 0),
                }
                for fo in unowned_ranked[:limit]
            ]
            click.echo(
                to_json(
                    json_envelope(
                        "codeowners",
                        budget=budget,
                        summary={
                            "verdict": verdict,
                            "total_files": total_files,
                            "owned_files": owned_count,
                            "coverage_pct": coverage_pct,
                            "total_owners": len(owner_files),
                            "codeowners_path": co_relpath,
                        },
                        owners=owner_dist,
                        unowned=unowned_items,
                        drift=drift_files[:limit],
                    )
                )
            )
            return

        # --- Text output ---
        click.echo(f"VERDICT: {verdict}")
        click.echo()

        # Ownership distribution
        if owner_dist:
            click.echo("  Ownership Distribution:")
            tbl_rows = []
            for od in owner_dist:
                areas_str = ", ".join(od["key_areas"])
                tbl_rows.append([od["name"], str(od["files"]), f"{od['pct']}%", areas_str])
            click.echo(format_table(["Owner", "Files", "%", "Key Areas"], tbl_rows, budget=limit))
            click.echo()

        # Coverage summary
        click.echo("  Coverage Summary:")
        click.echo(f"    Total files:   {total_files}")
        click.echo(f"    Owned files:   {owned_count} ({coverage_pct}%)")
        click.echo(f"    Unowned files: {unowned_count} ({round(100 - coverage_pct, 1)}%)")
        click.echo()

        # Top unowned files
        if unowned_ranked:
            click.echo("  Top Unowned Files (by PageRank):")
            tbl_rows = []
            for fo in unowned_ranked[:10]:
                pr = pagerank_map.get(fo["file_id"], 0)
                deps = dependents_map.get(fo["file_id"], 0)
                tbl_rows.append([fo["path"], f"{pr:.4f}", str(deps)])
            click.echo(format_table(["File", "PageRank", "Dependents"], tbl_rows))
            if len(unowned_ranked) > 10:
                click.echo(f"    (+{len(unowned_ranked) - 10} more unowned)")
            click.echo()

        # Ownership concentration
        if owner_dist:
            top_owner_pct = owner_dist[0]["pct"]
            # How many owners to cover owned_count?
            cumulative = 0
            bus_factor_owners = 0
            for od in owner_dist:
                cumulative += od["files"]
                bus_factor_owners += 1
                if cumulative >= owned_count * 0.8:
                    break
            click.echo("  Ownership Concentration:")
            click.echo(f"    Top owner covers: {top_owner_pct}% of files")
            click.echo(f"    Bus factor risk: {bus_factor_owners} owners cover 80% of owned files")
            click.echo()

        # Ownership drift
        if drift_files:
            click.echo(f"  Ownership Drift ({len(drift_files)} files):")
            tbl_rows = []
            for df in drift_files[:10]:
                declared = ", ".join(df["declared_owners"])
                tbl_rows.append([df["path"], declared, df["top_contributor"]])
            click.echo(format_table(["File", "Declared Owner", "Top Contributor"], tbl_rows))
            if len(drift_files) > 10:
                click.echo(f"    (+{len(drift_files) - 10} more drift)")
