#!/usr/bin/env python3
"""
LANGKAH 3 — Matching: memisahkan objek yang SUDAH terdaftar dari yang benar-benar BARU.

Tantangan utamanya perbedaan nama. Di register tercatat "CV. MAJU BERSAMA (WARMINDO)",
di Google Maps "Warmindo Mang Udin 24 Jam". Skrip ini memakai:

  1. Normalisasi nama — huruf kecil, buang tanda baca dan bentuk badan usaha (CV, PT, UD ...).
  2. Kemiripan nama berbobot token (IDF) — kata langka seperti "udin" lebih menentukan
     daripada kata umum seperti "warung" atau "hotel".
  3. Bukti lokasi menentukan ambang:
       - jarak < 150 m (bila register punya koordinat)  → ambang paling longgar
       - kecamatan sama                                  → ambang sedang
       - lintas kecamatan                                 → ambang ketat

Status keluaran:
  TERDAFTAR      skor ≥ ambang  → sudah ada di register (bukan target ekstensifikasi)
  PERLU_CEK      skor sedikit di bawah ambang → periksa manual oleh petugas
  KANDIDAT_BARU  tidak ada padanan → calon WP baru

Kebutuhan: Python 3.9+, tanpa pustaka tambahan.

Contoh:
  python3 3_cocokkan_register.py --objek bersih.csv \
      --register contoh/contoh_register.csv --keluaran hasil_cocok.csv

Kolom register yang dibaca: npwpd, nama_wp, kecamatan, dan opsional lat, lng.
Nama kolom berbeda? Pakai --kolom-npwpd, --kolom-nama, --kolom-kecamatan.
"""

import argparse
import csv
import math
import re
from collections import defaultdict

BADAN_USAHA = {"cv", "pt", "ud", "tbk", "pd", "fa", "koperasi", "kop", "yayasan", "persero"}
# Kata yang hampir tak punya daya pembeda. Jangan buang terlalu banyak —
# IDF sudah menurunkan bobot kata umum secara otomatis.
KATA_HENTI = {"dan", "di", "the", "of", "&", "jl", "jalan", "no", "24", "jam"}


def token(nama):
    nama = nama.lower()
    nama = re.sub(r"[^a-z0-9 ]+", " ", nama)
    return {t for t in nama.split() if t not in BADAN_USAHA and t not in KATA_HENTI and len(t) > 1}


def jarak_meter(lat1, lng1, lat2, lng2):
    r = 6371000
    p1, p2 = math.radians(lat1), math.radians(lat2)
    a = (math.sin((p2 - p1) / 2) ** 2
         + math.cos(p1) * math.cos(p2) * math.sin(math.radians(lng2 - lng1) / 2) ** 2)
    return 2 * r * math.asin(math.sqrt(a))


def angka(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return None


def kemiripan(ta, tb, idf):
    """Kemiripan berbobot: bobot token bersama ÷ bobot token himpunan yang lebih kecil.

    Memakai himpunan yang lebih kecil sebagai penyebut supaya
    "Warmindo Mang Udin" tetap cocok dengan "CV Maju Bersama Warmindo Mang Udin".
    """
    if not ta or not tb:
        return 0.0
    bersama = sum(idf[t] for t in ta & tb)
    penyebut = min(sum(idf[t] for t in ta), sum(idf[t] for t in tb))
    return bersama / penyebut if penyebut else 0.0


def main():
    p = argparse.ArgumentParser(description="Cocokkan objek bersih dengan register WP.")
    p.add_argument("--objek", required=True, help="keluaran langkah 2")
    p.add_argument("--register", required=True, help="CSV register WP dari SIMPATDA/e-PAD")
    p.add_argument("--keluaran", default="hasil_cocok.csv")
    p.add_argument("--kolom-npwpd", default="npwpd")
    p.add_argument("--kolom-nama", default="nama_wp")
    p.add_argument("--kolom-kecamatan", default="kecamatan")
    p.add_argument("--ambang-dekat", type=float, default=0.45, help="bila jarak < 150 m")
    p.add_argument("--ambang-kecamatan", type=float, default=0.60, help="bila kecamatan sama")
    p.add_argument("--ambang-lintas", type=float, default=0.85, help="bila beda kecamatan")
    p.add_argument("--zona-cek", type=float, default=0.15,
                   help="selisih di bawah ambang yang masih ditandai PERLU_CEK")
    a = p.parse_args()

    with open(a.objek, encoding="utf-8") as f:
        objek = list(csv.DictReader(f))
    with open(a.register, encoding="utf-8") as f:
        register = list(csv.DictReader(f))

    reg = []
    for r in register:
        reg.append({
            "npwpd": r[a.kolom_npwpd],
            "nama": r[a.kolom_nama],
            "kec": (r.get(a.kolom_kecamatan) or "").strip().lower(),
            "lat": angka(r.get("lat")),
            "lng": angka(r.get("lng")),
            "tok": token(r[a.kolom_nama]),
        })

    # IDF dihitung dari gabungan register + objek, supaya kata yang umum
    # di salah satu sisi tetap dianggap umum.
    df = defaultdict(int)
    semua_tok = [r["tok"] for r in reg] + [token(o["nama"]) for o in objek]
    for ts in semua_tok:
        for t in ts:
            df[t] += 1
    n = len(semua_tok)
    idf = defaultdict(lambda: math.log(n + 1), {t: math.log((n + 1) / (c + 0.5)) for t, c in df.items()})

    # Indeks terbalik: hanya bandingkan pasangan yang berbagi minimal satu token.
    indeks = defaultdict(list)
    for i, r in enumerate(reg):
        for t in r["tok"]:
            indeks[t].append(i)

    hitung = defaultdict(int)
    for o in objek:
        to = token(o["nama"])
        kec_o = (o.get("kecamatan") or "").strip().lower()
        lat_o, lng_o = angka(o.get("lat")), angka(o.get("lng"))

        calon = {i for t in to for i in indeks.get(t, [])}
        terbaik = None  # (selisih_terhadap_ambang, skor, ambang, idx, bukti)
        for i in calon:
            r = reg[i]
            skor = kemiripan(to, r["tok"], idf)
            if r["lat"] is not None and lat_o is not None and \
                    jarak_meter(lat_o, lng_o, r["lat"], r["lng"]) < 150:
                ambang, bukti = a.ambang_dekat, "jarak<150m"
            elif kec_o and r["kec"] == kec_o:
                ambang, bukti = a.ambang_kecamatan, "kecamatan sama"
            else:
                ambang, bukti = a.ambang_lintas, "lintas kecamatan"
            kandidat = (skor - ambang, skor, ambang, i, bukti)
            if terbaik is None or kandidat > terbaik:
                terbaik = kandidat

        if terbaik and terbaik[0] >= 0:
            status = "TERDAFTAR"
        elif terbaik and terbaik[0] >= -a.zona_cek:
            status = "PERLU_CEK"
        else:
            status = "KANDIDAT_BARU"
        hitung[status] += 1

        o["status"] = status
        if terbaik and status != "KANDIDAT_BARU":
            r = reg[terbaik[3]]
            o["npwpd_padanan"], o["nama_padanan"] = r["npwpd"], r["nama"]
            o["skor_kemiripan"], o["bukti_lokasi"] = f"{terbaik[1]:.2f}", terbaik[4]
        else:
            o["npwpd_padanan"] = o["nama_padanan"] = o["skor_kemiripan"] = o["bukti_lokasi"] = ""

    kolom = list(objek[0].keys())
    with open(a.keluaran, "w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=kolom)
        w.writeheader()
        w.writerows(objek)

    print(f"Objek dicocokkan: {len(objek)} | register: {len(reg)} WP")
    for s in ("TERDAFTAR", "PERLU_CEK", "KANDIDAT_BARU"):
        print(f"  {s:<14} {hitung[s]:>6}")
    print(f"Disimpan ke {a.keluaran}")


if __name__ == "__main__":
    main()
