#!/usr/bin/env python3
"""Scan a takeoff/spec PDF (or text) and compare to ACCI systems library pricing."""
from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
LIB_PATH = ROOT / "library" / "systems.json"
SCANS = ROOT / "scans"


def load_lib():
    return json.loads(LIB_PATH.read_text())


def pdf_text(path: Path, max_pages: int = 40) -> str:
    import fitz

    doc = fitz.open(path)
    parts = []
    for i, page in enumerate(doc):
        if i >= max_pages:
            break
        parts.append(page.get_text("text"))
    doc.close()
    return "\n".join(parts)


def extract_sf(text: str) -> list[float]:
    vals = []
    for m in re.finditer(
        r"(\d{1,3}(?:,\d{3})*(?:\.\d+)?|\d{3,7}(?:\.\d+)?)\s*(?:s\.?f\.?|sf|sq\.?\s*ft\.?|square\s*feet)",
        text,
        re.I,
    ):
        try:
            v = float(m.group(1).replace(",", ""))
        except ValueError:
            continue
        if 50 <= v <= 5_000_000:
            vals.append(v)
    return vals


def score_system(text_l: str, system: dict) -> tuple[int, list[str]]:
    score = 0
    hits = []
    for kw in system.get("spec_keywords") or []:
        if kw.lower() in text_l:
            score += 3 if len(kw) > 6 else 2
            hits.append(kw)
    for u in system.get("use_cases") or []:
        token = u.lower().split()[0]
        if len(token) > 4 and token in text_l:
            score += 1
            hits.append(u)
    fam = system.get("family", "")
    name = system.get("name", "")
    blob = fam + " " + name + " " + " ".join(system.get("spec_keywords") or [])

    checks = [
        (r"esd|conductive|dissipative|electrostatic", "ESD", 5),
        (r"\bflake\b|decorative flake|decoflake|decodur flake", "flake", 4),
        (r"\bquartz\b|deco quartz|glass broadcast", "quartz", 4),
        (r"novolac|chemical resistant|secondary containment", "novolac", 5),
        (r"troweled|mortar|multidur|tg46|screed", "troweled", 4),
        (r"sealed concrete|concrete sealer|polyaspartic sealer", "sealed", 4),
        (r"thin[- ]?finish|overlay|broom finish", "thin-finish", 4),
        (r"metallic|reflector", "metallic", 3),
        (r"ufgs|09\s*67\s*23|fuel resistive|hangar", "federal/hangar", 2),
    ]
    for pat, label, pts in checks:
        if re.search(pat, text_l, re.I) and re.search(label.split("/")[0], blob, re.I):
            score += pts
            hits.append(label)
    # manufacturer named in spec
    mfr = (system.get("manufacturer") or "").lower()
    if mfr and mfr.split()[0] in text_l:
        score += 2
        hits.append(system.get("manufacturer"))
    return score, sorted(set(hits))


def build_report(text: str, source: str, waste: float = 0.10) -> dict:
    lib = load_lib()
    text_l = text.lower()
    sfs = extract_sf(text)
    primary_sf = max(sfs) if sfs else None

    ranked = []
    for s in lib["systems"]:
        score, hits = score_system(text_l, s)
        if score <= 0:
            continue
        mat = (s.get("material_per_sf") or {}).get("median")
        sell = (s.get("sell_per_sf") or {}).get("default")
        row = {
            "id": s["id"],
            "name": s["name"],
            "manufacturer": s["manufacturer"],
            "family": s["family"],
            "score": score,
            "hits": hits,
            "material_per_sf_median": mat,
            "material_band": s.get("material_per_sf"),
            "sell_per_sf_default": sell,
            "docs": s.get("docs") or [],
            "proof_jobs": s.get("proof_jobs") or [],
            "summary": s.get("summary"),
            "alt_equals": s.get("alt_equals") or [],
        }
        if primary_sf and mat is not None:
            row["est_material_total"] = round(primary_sf * (1 + waste) * mat, 2)
        if primary_sf and sell is not None:
            row["est_sell_total"] = round(primary_sf * sell, 2)
            if row.get("est_material_total") is not None:
                row["est_gross_after_material"] = round(row["est_sell_total"] - row["est_material_total"], 2)
        ranked.append(row)
    ranked.sort(key=lambda r: (-r["score"], r["name"]))

    return {
        "source": source,
        "primary_sf_guess": primary_sf,
        "sf_mentions": sfs[:20],
        "waste_factor": waste,
        "match_count": len(ranked),
        "matches": ranked[:12],
        "notes": [
            "Scores are keyword heuristics — always read the BOD and TDS.",
            "Material totals use median mat $/sf × SF × (1+waste). Sell uses library default.",
            "Labor/travel/mobilization not included.",
        ],
    }


def main():
    ap = argparse.ArgumentParser(description="Match takeoff/spec to ACCI systems library")
    ap.add_argument("path", nargs="?", help="PDF or text file to scan")
    ap.add_argument("--text", help="Raw text instead of file")
    ap.add_argument("--waste", type=float, default=0.10, help="Material waste factor (default 0.10)")
    ap.add_argument("--out", help="Write JSON report path")
    ap.add_argument("--md", action="store_true", help="Print markdown summary")
    args = ap.parse_args()

    if args.text:
        text, source = args.text, "inline-text"
    elif args.path:
        p = Path(args.path).expanduser().resolve()
        if not p.exists():
            print(f"File not found: {p}", file=sys.stderr)
            sys.exit(1)
        if p.suffix.lower() == ".pdf":
            text = pdf_text(p)
        else:
            text = p.read_text(errors="replace")
        source = str(p)
    else:
        ap.print_help()
        sys.exit(2)

    report = build_report(text, source, waste=args.waste)
    SCANS.mkdir(parents=True, exist_ok=True)
    out = Path(args.out) if args.out else SCANS / f"scan_{Path(source).stem[:40]}.json"
    if not args.out and source == "inline-text":
        out = SCANS / "scan_inline.json"
    out.write_text(json.dumps(report, indent=2))

    if args.md or True:
        print(f"# Spec / takeoff match\n")
        print(f"- Source: `{report['source']}`")
        print(f"- Primary SF guess: **{report['primary_sf_guess'] or 'n/a'}**")
        print(f"- Matches: {report['match_count']}")
        print()
        if not report["matches"]:
            print("No keyword matches. Add resinous / flake / ESD / quartz language.")
        for i, m in enumerate(report["matches"][:8], 1):
            print(f"## {i}. {m['name']} (score {m['score']})")
            print(f"- {m['manufacturer']} · {m['family']}")
            print(f"- Hits: {', '.join(m['hits'][:8]) or '—'}")
            print(f"- Material median: ${m['material_per_sf_median']}/sf" if m.get("material_per_sf_median") is not None else "- Material: n/a")
            print(f"- Sell default: ${m['sell_per_sf_default']}/sf" if m.get("sell_per_sf_default") is not None else "- Sell: n/a")
            if m.get("est_material_total") is not None:
                print(f"- Est material (w/ waste): ${m['est_material_total']:,.2f}")
            if m.get("est_sell_total") is not None:
                print(f"- Est sell: ${m['est_sell_total']:,.2f}")
            if m.get("est_gross_after_material") is not None:
                print(f"- Sell − material: ${m['est_gross_after_material']:,.2f}")
            docs = m.get("docs") or []
            if docs:
                print(f"- Docs: {docs[0].get('path')}")
            print()
        print(f"JSON report: `{out}`")

    print(json.dumps({"ok": True, "out": str(out), "matches": report["match_count"], "primary_sf": report["primary_sf_guess"]}))


if __name__ == "__main__":
    main()
