"""
lana — Logical Attribute Node Aligner
======================================

Two ways to use it:

  lana.compare(a, b, ...)        -> head-to-head verdict between two records
  lana.rank([a, b, c, ...], ...) -> ranked leaderboard across many records

Domain-agnostic: job candidates, products, ML model runs, vehicles,
portfolios, game characters — anything with comparable fields.

Quick start
-----------
    from ablevlabs import lana

    a = {"name": "Zoro",  "power": 9100, "speed": 80}
    b = {"name": "Sanji", "power": 8800, "speed": 95}

    lana.show(lana.compare(a, b, priority={"power": 5, "speed": 3}))

Results are plain dicts. Turn them into other shapes with
to_json / to_csv / to_markdown / to_df, or write to disk with save().
"""

from __future__ import annotations

import json as _json
import warnings
from typing import Any, Iterable, Optional, Union

__version__ = "0.2.0"
__all__ = [
    "compare",
    "rank",
    "show",
    "to_json",
    "to_csv",
    "to_markdown",
    "to_df",
    "save",
    "PRIORITY_MIN",
    "PRIORITY_MAX",
]

PRIORITY_MIN = 1
PRIORITY_MAX = 5
_NAME_KEYS = ("name", "model", "id", "label", "title")


# --------------------------------------------------------------------------- #
# helpers
# --------------------------------------------------------------------------- #
def _is_number(v: Any) -> bool:
    """True for real numbers only. Excludes bool (in Python, True == 1)."""
    return isinstance(v, (int, float)) and not isinstance(v, bool)


def _normalize(record: dict, aliases: dict) -> dict:
    return {aliases.get(k, k): v for k, v in record.items()}


def _auto_name(record: dict, default: str) -> str:
    for k in _NAME_KEYS:
        v = record.get(k)
        if isinstance(v, str) and v:
            return v
    return default


def _check_priority(priority: dict) -> None:
    for field, p in priority.items():
        if (
            isinstance(p, bool)
            or not isinstance(p, (int, float))
            or not (PRIORITY_MIN <= p <= PRIORITY_MAX)
        ):
            raise ValueError(
                f"priority for '{field}' must be a number from "
                f"{PRIORITY_MIN} to {PRIORITY_MAX} (got {p!r}). "
                f"1 = barely matters, 5 = matters most."
            )


# --------------------------------------------------------------------------- #
# compare — head-to-head between TWO records
# --------------------------------------------------------------------------- #
def compare(
    a: dict,
    b: dict,
    name_a: Optional[str] = None,
    name_b: Optional[str] = None,
    priority: Optional[dict] = None,
    lower_better: Optional[Iterable[str]] = None,
    aliases: Optional[dict] = None,
) -> dict:
    """Compare two records field by field and return a clean dict."""
    aliases = aliases or {}
    a = _normalize(a, aliases)
    b = _normalize(b, aliases)

    name_a = name_a if name_a is not None else _auto_name(a, "A")
    name_b = name_b if name_b is not None else _auto_name(b, "B")

    priority = priority or {}
    lower_better = set(lower_better or [])
    _check_priority(priority)

    fields: dict = {}
    numeric_fields: set = set()
    score_a = 0.0
    score_b = 0.0

    for key in sorted(set(a) | set(b)):
        in_a, in_b = key in a, key in b
        if not (in_a and in_b):
            fields[key] = {
                name_a: a.get(key),
                name_b: b.get(key),
                "only_in": name_a if in_a else name_b,
            }
            continue
        va, vb = a[key], b[key]

        if _is_number(va) and _is_number(vb):
            numeric_fields.add(key)
            if key in lower_better:
                a_better, b_better = va < vb, va > vb
            else:
                a_better, b_better = va > vb, va < vb
            winner = name_a if a_better else name_b if b_better else "tie"
            entry = {
                name_a: va,
                name_b: vb,
                "delta": round(va - vb, 6),
                "winner": winner,
            }
            if key in priority:
                p = priority[key]
                if winner == name_a:
                    score_a += p
                elif winner == name_b:
                    score_b += p
                else:
                    score_a += p / 2
                    score_b += p / 2
                entry["priority"] = p
            fields[key] = entry

        elif isinstance(va, dict) and isinstance(vb, dict):
            shared = {}
            for sk in sorted(set(va) & set(vb)):
                sva, svb = va[sk], vb[sk]
                if _is_number(sva) and _is_number(svb):
                    w = name_a if sva > svb else name_b if svb > sva else "tie"
                    shared[sk] = {
                        name_a: sva,
                        name_b: svb,
                        "delta": round(sva - svb, 6),
                        "winner": w,
                    }
                else:
                    shared[sk] = {name_a: sva, name_b: svb, "match": sva == svb}
            fields[key] = {
                "nested": True,
                "shared": shared,
                f"only_{name_a}": {sk: va[sk] for sk in sorted(set(va) - set(vb))},
                f"only_{name_b}": {sk: vb[sk] for sk in sorted(set(vb) - set(va))},
            }
        else:
            fields[key] = {name_a: va, name_b: vb, "match": va == vb}

    all_keys = set(a) | set(b)
    for field in priority:
        if field not in all_keys:
            warnings.warn(
                f"priority set for '{field}', but no such field exists "
                f"in either record — it was ignored. (Check the spelling.)",
                stacklevel=2,
            )
        elif field not in numeric_fields:
            warnings.warn(
                f"priority set for '{field}', but it isn't numeric in both "
                f"records, so it can't be scored — it was ignored.",
                stacklevel=2,
            )
    for field in lower_better:
        if field not in all_keys:
            warnings.warn(
                f"lower_better lists '{field}', but no such field exists — "
                f"ignored. (Check the spelling.)",
                stacklevel=2,
            )

    verdict = None
    if priority:
        total = score_a + score_b
        verdict = {
            "winner": (
                name_a if score_a > score_b else name_b if score_b > score_a else "tie"
            ),
            name_a: round(100 * score_a / total, 1) if total else 0,
            name_b: round(100 * score_b / total, 1) if total else 0,
        }

    return {"matchup": [name_a, name_b], "fields": fields, "verdict": verdict}


# --------------------------------------------------------------------------- #
# rank — leaderboard across MANY records (new in 0.2.0)
# --------------------------------------------------------------------------- #
def _as_named(items, aliases) -> dict:
    """Turn a list of records (or a {name: record} dict) into {name: record}."""
    out: dict = {}
    if isinstance(items, dict):
        for name, rec in items.items():
            out[str(name)] = _normalize(rec, aliases)
    else:
        for idx, rec in enumerate(items, 1):
            rec_n = _normalize(rec, aliases)
            name = _auto_name(rec_n, f"Item {idx}")
            base, k = name, 2
            while name in out:  # avoid duplicate labels
                name = f"{base} ({k})"
                k += 1
            out[name] = rec_n
    return out


def rank(
    items: Union[list, dict],
    priority: Optional[dict] = None,
    lower_better: Optional[Iterable[str]] = None,
    aliases: Optional[dict] = None,
) -> dict:
    """
    Rank 2+ records into a leaderboard using rank-weighted scoring.

    items        : a list of records  [a, b, c, ...]  OR  a {name: record} dict.
    priority     : {field: 1-5} — how much each field matters. If omitted,
                   every numeric field shared by all items counts equally.
    lower_better : fields where a SMALLER number is better (price, latency...).
    aliases      : {variant: canonical} to reconcile differing field names.

    Scoring (method B, rank-weighted): on each field, items earn points by
    placement — best gets the field's full priority, worst gets 0, evenly
    spaced; ties split the average. Points sum across fields. For exactly two
    items this matches compare().

    Returns a dict: {ranking, winner, scored_fields}.
    """
    aliases = aliases or {}
    named = _as_named(items, aliases)
    if len(named) < 2:
        raise ValueError("rank() needs at least 2 items to rank.")

    lower_better = set(lower_better or [])

    if priority:
        _check_priority(priority)
        all_keys = set().union(*[set(r) for r in named.values()])
        for f in priority:
            if f not in all_keys:
                warnings.warn(
                    f"priority set for '{f}', but no item has that field — "
                    f"ignored. (Check the spelling.)",
                    stacklevel=2,
                )
        score_fields = list(priority)
    else:
        common = None
        for rec in named.values():
            nums = {k for k, v in rec.items() if _is_number(v)}
            common = nums if common is None else (common & nums)
        score_fields = sorted(common or [])
        priority = {f: 1 for f in score_fields}

    scores = {name: 0.0 for name in named}

    for f in score_fields:
        p = priority[f]
        present = [
            (name, rec[f])
            for name, rec in named.items()
            if f in rec and _is_number(rec[f])
        ]
        M = len(present)
        if M < 2:
            continue
        present.sort(key=lambda nv: nv[1], reverse=(f not in lower_better))

        def pts(r, _M=M, _p=p):  # points for placement r (0 = best)
            return _p * (_M - 1 - r) / (_M - 1)

        i = 0
        while i < M:  # average points across tied values
            j = i
            while j < M and present[j][1] == present[i][1]:
                j += 1
            avg = sum(pts(r) for r in range(i, j)) / (j - i)
            for r in range(i, j):
                scores[present[r][0]] += avg
            i = j

    order = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)
    ranking = [
        {
            "name": name,
            "rank": idx,
            "score": round(sc, 4),
            "values": {f: named[name].get(f) for f in score_fields},
        }
        for idx, (name, sc) in enumerate(order, 1)
    ]

    return {
        "ranking": ranking,
        "winner": order[0][0] if order else None,
        "scored_fields": list(score_fields),
    }


# --------------------------------------------------------------------------- #
# shared flatteners for the exporters
# --------------------------------------------------------------------------- #
def _rows_compare(result: dict):
    a, b = result["matchup"]
    rows = []
    for field, d in result["fields"].items():
        if d.get("nested"):
            for sk, sd in d["shared"].items():
                rows.append(
                    {
                        "field": f"{field}.{sk}",
                        a: sd[a],
                        b: sd[b],
                        "delta": sd.get("delta"),
                        "winner": sd.get(
                            "winner", "match" if sd.get("match") else "differ"
                        ),
                        "priority": None,
                    }
                )
            for sk, val in d[f"only_{a}"].items():
                rows.append(
                    {
                        "field": f"{field}.{sk}",
                        a: val,
                        b: None,
                        "delta": None,
                        "winner": f"only {a}",
                        "priority": None,
                    }
                )
            for sk, val in d[f"only_{b}"].items():
                rows.append(
                    {
                        "field": f"{field}.{sk}",
                        a: None,
                        b: val,
                        "delta": None,
                        "winner": f"only {b}",
                        "priority": None,
                    }
                )
        elif "only_in" in d:
            rows.append(
                {
                    "field": field,
                    a: d[a],
                    b: d[b],
                    "delta": None,
                    "winner": f"only {d['only_in']}",
                    "priority": None,
                }
            )
        elif "winner" in d:
            rows.append(
                {
                    "field": field,
                    a: d[a],
                    b: d[b],
                    "delta": d.get("delta"),
                    "winner": d["winner"],
                    "priority": d.get("priority"),
                }
            )
        else:
            rows.append(
                {
                    "field": field,
                    a: d[a],
                    b: d[b],
                    "delta": None,
                    "winner": "match" if d["match"] else "differ",
                    "priority": None,
                }
            )
    return rows, ["field", a, b, "delta", "winner", "priority"]


def _table(result: dict):
    """Return (rows, columns) for whichever kind of result this is."""
    if "ranking" in result:
        fields = list(result["scored_fields"])
        cols = ["rank", "name"] + fields + ["score"]
        rows = []
        for e in result["ranking"]:
            row = {"rank": e["rank"], "name": e["name"], "score": round(e["score"], 2)}
            for f in fields:
                row[f] = e["values"].get(f)
            rows.append(row)
        return rows, cols
    return _rows_compare(result)


# --------------------------------------------------------------------------- #
# display + exporters (work on both compare and rank results)
# --------------------------------------------------------------------------- #
def show(result: dict) -> None:
    """Pretty-print a result: a scorecard for compare(), a leaderboard for rank()."""
    if "ranking" in result:
        print("\n  LEADERBOARD")
        print("  " + "-" * 42)
        for e in result["ranking"]:
            star = "*" if e["rank"] == 1 else " "
            print(f"  {star} {e['rank']}. {e['name']:<16}{round(e['score'], 2):>7} pts")
        print()
        return

    a, b = result["matchup"]
    print(f"\n  {a}  vs  {b}")
    print("  " + "-" * 50)
    for field, d in result["fields"].items():
        if d.get("nested"):
            print(f"  {field}:")
            for sk, sd in d["shared"].items():
                if "winner" in sd:
                    arrow = (
                        "<--"
                        if sd["winner"] == a
                        else "-->" if sd["winner"] == b else "==="
                    )
                    print(f"    {sk:<13}{sd[a]:>9} {arrow} {sd[b]:<9}")
                else:
                    mark = "==" if sd["match"] else "!="
                    print(f"    {sk:<13}{str(sd[a]):>9} {mark}  {sd[b]:<9}")
            if d[f"only_{a}"]:
                print(f"    only {a}: {', '.join(d[f'only_{a}'])}")
            if d[f"only_{b}"]:
                print(f"    only {b}: {', '.join(d[f'only_{b}'])}")
        elif "only_in" in d:
            print(f"  {field:<15}(only in {d['only_in']})")
        elif "winner" in d:
            arrow = "<--" if d["winner"] == a else "-->" if d["winner"] == b else "==="
            note = f"  (priority {d['priority']})" if "priority" in d else ""
            print(f"  {field:<15}{d[a]:>9} {arrow} {d[b]:<9}{note}")
        else:
            mark = "==" if d["match"] else "!="
            print(f"  {field:<15}{str(d[a]):>9} {mark}  {d[b]:<9}")
    if result["verdict"]:
        v = result["verdict"]
        print("  " + "-" * 50)
        print(f"  OVERALL: {v['winner']} wins   ({a} {v[a]}%  /  {b} {v[b]}%)")
    print()


def to_json(result: dict, indent: int = 2) -> str:
    """Return the result as a pretty JSON string."""
    return _json.dumps(result, indent=indent)


def to_csv(result: dict) -> str:
    """Return the result as a CSV string (opens in Excel / Google Sheets)."""
    import csv
    import io

    rows, cols = _table(result)
    buf = io.StringIO()
    w = csv.DictWriter(buf, fieldnames=cols)
    w.writeheader()
    for r in rows:
        w.writerow(r)
    return buf.getvalue()


def to_markdown(result: dict) -> str:
    """Return the result as a Markdown table (paste into docs / GitHub)."""
    rows, cols = _table(result)
    out = [
        "| " + " | ".join(str(c) for c in cols) + " |",
        "| " + " | ".join("---" for _ in cols) + " |",
    ]
    for r in rows:
        out.append(
            "| "
            + " | ".join("" if r.get(c) is None else str(r.get(c)) for c in cols)
            + " |"
        )
    md = "\n".join(out)
    if result.get("verdict"):
        v = result["verdict"]
        a, b = result["matchup"]
        md += f"\n\n**Overall: {v['winner']} wins** ({a} {v[a]}% / {b} {v[b]}%)"
    elif result.get("ranking"):
        md += f"\n\n**Winner: {result['winner']}**"
    return md


def to_df(result: dict):
    """Return the result as a pandas DataFrame (for analysis / sorting)."""
    try:
        import pandas as pd
    except ImportError as exc:  # pragma: no cover
        raise ImportError(
            "to_df() needs pandas. Install it with:  pip install pandas"
        ) from exc
    rows, cols = _table(result)
    return pd.DataFrame(rows, columns=cols)


def save(result: dict, path: str, indent: int = 2) -> str:
    """
    Write the result to disk. Format is chosen from the file extension:
      .json -> JSON   .csv -> CSV   .md -> Markdown   (anything else -> JSON)
    Returns the path written.
    """
    low = path.lower()
    if low.endswith(".csv"):
        text = to_csv(result)
    elif low.endswith(".md"):
        text = to_markdown(result)
    else:
        text = to_json(result, indent=indent)
    with open(path, "w", newline="", encoding="utf-8") as f:
        f.write(text)
    return path
