#!/usr/bin/env python3
"""
FSA Lab — Toolkit Forensik (Python Workbench)
Jalankan lokal / Colab. Tiga screen siap pakai untuk screening laporan keuangan.

Contoh:
    python3 fsa_toolkit.py
"""
import math
import xml.etree.ElementTree as ET


# ---------- 1. Beneish M-score: probabilitas manipulasi laba ----------
def beneish_mscore(dsri, gmi, aqi, sgi, depi, sgai, lvgi, tata):
    """8 rasio Beneish. M > -1.78 = kemungkinan manipulator (flag)."""
    m = (-4.84 + 0.92*dsri + 0.528*gmi + 0.404*aqi + 0.892*sgi
         + 0.115*depi - 0.172*sgai + 4.679*tata - 0.327*lvgi)
    return m, ("FLAG — kemungkinan manipulator" if m > -1.78
               else "PASS — tidak terindikasi")


# ---------- 2. DSO screen: piutang vs penjualan ----------
def dso_screen(receivables, revenue, prev_dso, threshold_days=15):
    """Flag bila DSO naik > threshold vs tahun lalu."""
    dso = receivables / revenue * 365 if revenue else 0
    drift = dso - prev_dso
    return dso, drift, ("FLAG — DSO melar %.1f hari" % drift if drift > threshold_days
                       else "PASS — DSO wajar")


# ---------- 3. Benford test: kealamian angka ----------
def benford_test(numbers):
    """Bandingkan distribusi digit pertama vs hukum Benford. Return max deviation."""
    benford = [math.log10(1 + 1/d) for d in range(1, 10)]
    digits = [int(str(abs(float(x))).lstrip("0").replace(".", "")[:1])
              for x in numbers if float(x) != 0]
    digits = [d for d in digits if 1 <= d <= 9]
    n = len(digits)
    if not n:
        return 0.0, "data kosong"
    dev = max(abs(digits.count(d)/n - benford[d-1]) for d in range(1, 10))
    return dev, ("FLAG — deviasi %.3f, selidiki" % dev if dev > 0.06
                 else "PASS — wajar")


# ---------- 4. Parser XBRL sederhana → 4 rasio forensik ----------
def xbrl_parse_ratios(xml_string):
    """Parse XML SEDERHANA berisi angka laporan (tag seperti <Revenue>,
    <NetIncome>, <TotalAssets>, <OperatingCashFlow>) lalu hitung:
    net margin, ROA, CFO/NI, dan accruals (NI - CFO).

    CONTOH OFFLINE — bukan koneksi live ke EDGAR. Untuk data live pakai
    SEC EDGAR API (https://www.sec.gov/cgi-bin/browse-edgar) lalu feed
    XML/JSON-nya ke fungsi ini.
    """
    root = ET.fromstring(xml_string)

    def num(tag):
        # cari tag apa adanya; abaikan namespace bila ada (suffix match)
        el = root.find(".//" + tag)
        if el is None:
            for e in root.iter():
                if e.tag == tag or e.tag.endswith("}" + tag):
                    el = e
                    break
        if el is None or el.text is None:
            raise ValueError("tag <%s> tidak ditemukan" % tag)
        return float(el.text.strip().replace(",", "").replace(" ", ""))

    rev = num("Revenue")
    ni = num("NetIncome")
    ta = num("TotalAssets")
    cfo = num("OperatingCashFlow")
    ratios = {
        "net_margin": ni / rev if rev else 0,
        "roa": ni / ta if ta else 0,
        "cfo_ni": cfo / ni if ni else 0,
        "accruals": ni - cfo,
    }
    return ratios


XBRL_CONTOH = """<Financials>
  <Revenue>520000</Revenue>
  <NetIncome>53200</NetIncome>
  <TotalAssets>461000</TotalAssets>
  <OperatingCashFlow>30800</OperatingCashFlow>
</Financials>"""


# ---------- 5. Duplicate detector: angka kembar = jejak invoice ganda ----------

def duplicate_detector(values, min_count=2):
    """Cari nilai yang muncul >= min_count kali dalam daftar angka jurnal.

    Duplikat persis dalam jumlah besar = red flag klasik: invoice digandakan,
    pembayaran dipecah agar lolos ambang otorisasi, atau round-tripping.
    Mengembalikan dict {nilai: jumlah_kemunculan} yang memenuhi ambang,
    diurut dari yang paling sering.
    """
    from collections import Counter
    c = Counter(values)
    dups = {v: n for v, n in c.items() if n >= min_count}
    return dict(sorted(dups.items(), key=lambda kv: -kv[1]))


# ---------- 6. Gap analysis: nomor dokumen yang hilang ----------

def gap_analysis(numbers):
    """Cari nomor urut yang HILANG dari deretan nomor dokumen (invoice, cek, PO).

    Nomor yang hilang = dokumen disembunyikan / dibatalkan diam-diam / dipakai
    dua kali. Mengembalikan list nomor yang hilang di antara min dan max.
    """
    if not numbers:
        return []
    lo, hi = min(numbers), max(numbers)
    have = set(numbers)
    return [n for n in range(lo, hi + 1) if n not in have]


# ---------- 7. Red-flag scan: bahasa MD&A yang menghindar ----------

REDFLAG_WORDS = [
    "challenging", "headwind", "one-time", "non-recurring", "restructuring",
    "strategic alternatives", "going concern", "material weakness",
    "restatement", "difficult market conditions", "unprecedented",
    "transformational", "right-sizing", "synergies", "pro forma",
]

def redflag_scan(text):
    """Hitung frasa penghindar (weasel words) dalam teks MD&A / catatan.

    Manajemen yang jujur memakai angka; yang bermasalah memakai eufemisme.
    Bukan bukti fraud — tapi daftar paragraf yang wajib dibaca ulang.
    Mengembalikan list (frasa, jumlah) yang ditemukan, terurut terbanyak.
    """
    t = text.lower()
    hits = [(w, t.count(w)) for w in REDFLAG_WORDS if w in t]
    return sorted(hits, key=lambda kv: -kv[1])


# ---------- 8. EDGAR live: tarik fakta XBRL emiten US langsung ----------

def fetch_edgar_facts(cik, timeout=30):
    """Tarik companyfacts XBRL dari SEC EDGAR API untuk satu emiten (LIVE).

    cik: string 10 digit, mis. "0000320193" (Apple). Daftar CIK:
    https://www.sec.gov/files/company_tickers.json
    Mengembalikan dict {Revenue, NetIncome, TotalAssets, OperatingCashFlow}
    periode FY terakhir (form 10-K), dalam USD, plus tahunnya.

    Jujur soal batas: butuh internet; SEC mewajibkan User-Agent yang wajar;
    angka = as-reported (belum dinormalisasi); rate-limit SEC ~10 req/detik.
    """
    import json, urllib.request
    cik = str(cik).zfill(10)
    url = "https://data.sec.gov/api/xbrl/companyfacts/CIK%s.json" % cik
    req = urllib.request.Request(url, headers={
        "User-Agent": "FSA-Lab-Edu/1.0 (pembelajaran analisis laporan keuangan)",
        "Accept": "application/json",
    })
    with urllib.request.urlopen(req, timeout=timeout) as r:
        data = json.load(r)
    facts = data["facts"]["us-gaap"]

    def latest_fy(tag):
        if tag not in facts:
            return None, None
        usd = facts[tag]["units"].get("USD", [])
        fy = [x for x in usd if x.get("form") == "10-K" and x.get("fp") == "FY"]
        if not fy:
            return None, None
        last = max(fy, key=lambda x: x["end"])
        return last["val"], last["end"][:4]

    rev_tags = ["RevenueFromContractWithCustomerExcludingAssessedTax",
                "RevenuesFromContractsWithCustomersExcludingAssessedTax",
                "Revenues", "SalesRevenueNet"]
    rev, yr = None, None
    for t in rev_tags:
        rev, yr = latest_fy(t)
        if rev is not None:
            break
    ni, _ = latest_fy("NetIncomeLoss")
    ta, _ = latest_fy("Assets")
    cfo, _ = latest_fy("NetCashProvidedByUsedInOperatingActivities")
    return {"cik": cik, "fiscal_year": yr, "Revenue": rev, "NetIncome": ni,
            "TotalAssets": ta, "OperatingCashFlow": cfo}


if __name__ == "__main__":
    # Contoh: PT Cemerlang Abadi (Modul 2) — angka ilustratif
    print("== Beneish M-score (contoh) ==")
    m, s = beneish_mscore(dsri=1.45, gmi=0.97, aqi=1.02, sgi=1.30,
                          depi=1.0, sgai=1.05, lvgi=0.96, tata=0.032)
    print("M = %.2f → %s" % (m, s))
    print("\n== DSO screen ==")
    dso, drift, s = dso_screen(receivables=104_000, revenue=520_000, prev_dso=50.2)
    print("DSO = %.1f hari → %s" % (dso, s))
    print("\n== Benford test (contoh umum — 12 angka terlalu sedikit untuk uji serius) ==")
    dev, s = benford_test([480000, 288000, 192000, 120000, 72000, 46800,
                           35000, 72000, 84000, 210000, 48000, 30000])
    print("deviasi maks = %.3f → %s" % (dev, s))

    print("\n== Duplicate detector (contoh: invoice digandakan?) ==")
    d = duplicate_detector([12000, 45000, 12000, 7800, 45000, 45000, 9100])
    print("duplikat:", d if d else "tidak ada")

    print("\n== Gap analysis (contoh: nomor invoice hilang) ==")
    g = gap_analysis([101, 102, 104, 105, 108])
    print("nomor hilang:", g if g else "tidak ada")

    print("\n== Red-flag scan (contoh MD&A) ==")
    rf = redflag_scan("In this challenging year with headwinds, we booked one-time "
                      "restructuring charges as part of strategic alternatives.")
    print("frasa penghindar:", rf if rf else "tidak ada")

    print("\n== Parser XBRL → rasio forensik (contoh offline; live = SEC EDGAR API) ==")
    r = xbrl_parse_ratios(XBRL_CONTOH)
    print("net margin = %.1f%%" % (r["net_margin"] * 100))
    print("ROA        = %.1f%%" % (r["roa"] * 100))
    print("CFO / NI   = %.2fx %s" % (r["cfo_ni"],
          "→ FLAG, laba tidak jadi kas" if r["cfo_ni"] < 1 else "→ PASS"))
    print("accruals (NI-CFO) = {:,.0f}".format(r["accruals"]))
