#!/usr/bin/env python3
"""
dupes — near-duplicate candidates for a note or a piece of text (vaultq verb; M4).

The other half of the autofile dedup guard: before creating a note (or when
sweeping hygiene), ask whether the vault already holds this content under a
slightly different name. Catches what `resolve` can't — resolve matches
*names*; dupes matches *content and titles* ("tokenized" vs "tokenised", a
re-dump of the same article, two drafts of one memo).

Signals (deterministic, stdlib-only, no API calls):
  cosine  TF-IDF cosine between the input and every note body (route's
          vectorizer — idf over the live corpus, ubiquitous terms skipped).
  title   difflib ratio between normalized titles (input title vs note
          title/aliases) — catches renamed twins with light bodies.

similarity = max(cosine, 0.95 × title-ratio); a hit reports both. Input may be
an existing note (name/alias/rel — compared body-vs-corpus, itself excluded) or
raw text / @file.

    python -m alexandria_vault.vault_core.query.vaultq dupes "<existing note title, raw text, or @file>"
    python -m alexandria_vault.vault_core.query.vaultq dupes "@/tmp/dump.md" --threshold 0.5
"""
import os
import sys
import json
import difflib
import argparse
from pathlib import Path

from .vaultgraph import find_vault_root  # noqa: E402
from . import resolve as _resolve  # noqa: E402
from . import find as _find  # noqa: E402
from . import route as _route  # noqa: E402

THRESHOLD = 0.60
TITLE_W = 0.95


def _norm_title(s):
    return _resolve.norm_name(os.path.splitext(os.path.basename(s))[0])


def dupes(root, query, threshold=THRESHOLD, limit=10):
    """Near-duplicates of `query` (an existing note name/rel, raw text, or
    @file). Returns {input: {kind, rel?, title?}, threshold,
    dupes: [{rel, title, type, similarity, cosine, title_sim, why}]}."""
    notes = _find.scan(root)
    meta = {n["rel"]: n for n in notes}

    # Is the query an existing note? (rel path, basename, or alias)
    rel = query if query in meta else None
    if rel is None and "\n" not in query:
        ql = query.casefold().removesuffix(".md")
        for n in notes:
            if n["title"].casefold() == ql or ql in (a.casefold() for a in n["aliases"]):
                rel = n["rel"]
                break

    if rel:
        src = meta[rel]
        text = src["title"] + "\n" + src["body"]
        in_title = src["title"]
        input_desc = {"kind": "note", "rel": rel, "title": in_title}
    else:
        # '@file' is a CLI-only affordance (route._read_input) and is NOT honored
        # here: a tool must never read files off caller input outside the vault jail.
        text = query
        # first markdown H1 or first non-empty line stands in as the title
        in_title = next((ln.lstrip("# ").strip() for ln in text.splitlines()
                         if ln.strip()), "")
        input_desc = {"kind": "text", "title": in_title[:80]}

    corpus = [n for n in notes if n["rel"] != rel]
    qv, nvecs = _route._vectorize(corpus, text)
    # ≥2 shared terms — a true near-dupe shares many; one rare term isn't a dupe
    cos = {r: round(sum(qv[t] * v[t] for t in v), 3)
           for r, v in nvecs.items() if len(v) >= 2}

    tnorm = _resolve.norm_name(in_title)
    rows = []
    for n in corpus:
        c = cos.get(n["rel"], 0.0)
        tsims = [difflib.SequenceMatcher(None, tnorm, _norm_title(x)).ratio()
                 for x in [n["title"]] + n["aliases"]] if tnorm else [0.0]
        t = round(max(tsims), 3)
        sim = round(max(c, TITLE_W * t), 3)
        if sim < threshold:
            continue
        why = []
        if c >= threshold:
            why.append(f"cosine:{c}")
        if TITLE_W * t >= threshold:
            why.append(f"title:{t}")
        rows.append({"rel": n["rel"], "title": n["title"], "type": n["type"],
                     "similarity": sim, "cosine": c, "title_sim": t,
                     "why": "+".join(why)})
    rows.sort(key=lambda r: (-r["similarity"], r["rel"]))
    return {"input": input_desc, "threshold": threshold, "dupes": rows[:limit]}


def print_pretty(out):
    i = out["input"]
    label = i.get("rel") or i.get("title") or "?"
    if not out["dupes"]:
        print(f'✓ no near-duplicates of {label!r} at ≥{out["threshold"]}')
        return
    print(f'{len(out["dupes"])} near-duplicate candidate(s) of {label!r} '
          f'(≥{out["threshold"]}):')
    for d in out["dupes"]:
        print(f'  {d["similarity"]:5.2f}  [[{d["title"]}]]  '
              f'({d["type"] or "?"})  {d["why"]}')


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("query", help="existing note (name/rel), raw text, or @file")
    ap.add_argument("--threshold", type=float, default=THRESHOLD)
    ap.add_argument("--limit", type=int, default=10)
    ap.add_argument("--pretty", action="store_true")
    args = ap.parse_args()
    out = dupes(find_vault_root(), args.query, threshold=args.threshold,
                limit=args.limit)
    print_pretty(out) if args.pretty else print(json.dumps(out, indent=1))


if __name__ == "__main__":
    main()
