from __future__ import annotations
import argparse, csv, html, json, math, re, statistics
from pathlib import Path

ROOT = Path(__file__).resolve().parent
METRICS = ["cpi_yoy", "ppi_yoy", "ipi_usd_yoy", "epi_usd_yoy"]

def numeric(raw):
    match = re.search(r"[-+]?\d+(?:\.\d+)?", str(raw).replace(",", ""))
    if not match: raise ValueError(f"no numeric value: {raw!r}")
    return float(match.group())

def read_csv(path):
    with path.open(encoding="utf-8-sig", newline="") as handle: return list(csv.DictReader(handle))

def write_csv(path, rows, fields):
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fields, lineterminator="\n"); writer.writeheader(); writer.writerows(rows)

def svg_line(path, title, rows, series, y_min, y_max):
    w,h=960,500; ml,mr,mt,mb=80,35,65,70; pw=w-ml-mr; ph=h-mt-mb
    colors=["#0f766e","#d97706","#2563eb","#9333ea"]
    def x(i): return ml + (pw*i/max(1,len(rows)-1))
    def y(v): return mt + ph*(y_max-v)/(y_max-y_min)
    parts=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">',
      '<rect width="100%" height="100%" fill="#fffdf7"/>', f'<text x="{ml}" y="34" font-family="sans-serif" font-size="24" font-weight="700">{html.escape(title)}</text>']
    for tick in range(math.ceil(y_min/5)*5, math.floor(y_max/5)*5+1, 5):
        yy=y(tick); parts += [f'<line x1="{ml}" y1="{yy:.2f}" x2="{w-mr}" y2="{yy:.2f}" stroke="#d6d3d1"/>', f'<text x="{ml-12}" y="{yy+5:.2f}" text-anchor="end" font-family="sans-serif" font-size="14">{tick}</text>']
    zero=y(0); parts.append(f'<line x1="{ml}" y1="{zero:.2f}" x2="{w-mr}" y2="{zero:.2f}" stroke="#57534e" stroke-width="1.5"/>')
    for idx,(field,label) in enumerate(series):
        pts=" ".join(f"{x(i):.2f},{y(float(r[field])):.2f}" for i,r in enumerate(rows)); color=colors[idx]
        parts += [f'<polyline points="{pts}" fill="none" stroke="{color}" stroke-width="3"/>',
          f'<line x1="{ml+idx*205}" y1="{h-26}" x2="{ml+idx*205+28}" y2="{h-26}" stroke="{color}" stroke-width="4"/>',
          f'<text x="{ml+idx*205+36}" y="{h-21}" font-family="sans-serif" font-size="14">{html.escape(label)}</text>']
    for i,r in enumerate(rows):
        if i%2==0 or i==len(rows)-1: parts.append(f'<text x="{x(i):.2f}" y="{h-50}" transform="rotate(45 {x(i):.2f} {h-50})" font-family="sans-serif" font-size="12">{r["period"]}</text>')
    parts.append('</svg>'); path.write_text("\n".join(parts)+"\n",encoding="utf-8")

def svg_bars(path, rows):
    w,h=960,500; ml,mr,mt,mb=85,35,75,85; pw=w-ml-mr; ph=h-mt-mb
    vals=[float(r["ppi_delta_pp"]) for r in rows]+[float(r["cpi_delta_pp"]) for r in rows]; lim=max(1,math.ceil(max(abs(v) for v in vals)*10)/10)
    def y(v): return mt+ph*(lim-v)/(2*lim)
    group=pw/max(1,len(rows)); bar=group*.28; zero=y(0)
    parts=[f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}">','<rect width="100%" height="100%" fill="#fffdf7"/>',
      f'<text x="{ml}" y="34" font-family="sans-serif" font-size="24" font-weight="700">機器快照減首發版：修訂差異（百分點）</text>',
      f'<line x1="{ml}" y1="{zero:.2f}" x2="{w-mr}" y2="{zero:.2f}" stroke="#57534e"/>']
    for i,r in enumerate(rows):
        cx=ml+group*(i+.5)
        for j,(field,color) in enumerate((("cpi_delta_pp","#0f766e"),("ppi_delta_pp","#d97706"))):
            v=float(r[field]); yy=y(v); top=min(zero,yy); height=max(1,abs(zero-yy)); xx=cx+(j-.5)*bar-bar/2
            parts.append(f'<rect x="{xx:.2f}" y="{top:.2f}" width="{bar:.2f}" height="{height:.2f}" fill="{color}"/>')
        parts.append(f'<text x="{cx:.2f}" y="{h-56}" transform="rotate(45 {cx:.2f} {h-56})" font-family="sans-serif" font-size="12">{r["period"]}</text>')
    parts += ['<rect x="85" y="458" width="20" height="10" fill="#0f766e"/><text x="112" y="468" font-family="sans-serif" font-size="14">CPI</text>',
      '<rect x="180" y="458" width="20" height="10" fill="#d97706"/><text x="207" y="468" font-family="sans-serif" font-size="14">PPI</text></svg>']
    path.write_text("\n".join(parts)+"\n",encoding="utf-8")

def run(output_root=ROOT):
    source=read_csv(ROOT/"data"/"price-index-yoy-release-vintage.csv")
    assert len(source)==19 and source[0]["period"]=="2025-01" and source[-1]["period"]=="2026-07"
    assert len({r["period"] for r in source})==len(source)
    rows=[]
    for r in source:
        row={"period":r["period"],**{m:round(float(r[m]),2) for m in METRICS}}
        row.update({"ppi_minus_cpi_pp":round(row["ppi_yoy"]-row["cpi_yoy"],2),"ipi_minus_cpi_pp":round(row["ipi_usd_yoy"]-row["cpi_yoy"],2),"epi_minus_cpi_pp":round(row["epi_usd_yoy"]-row["cpi_yoy"],2)})
        rows.append(row)
    out=output_root/"outputs"; figs=output_root/"figures"; out.mkdir(parents=True,exist_ok=True); figs.mkdir(parents=True,exist_ok=True)
    spread_fields=list(rows[0]); write_csv(out/"monthly-price-spreads.csv",rows,spread_fields)
    cross=read_csv(ROOT/"data"/"labor-ministry-macro-crosscheck.csv"); release={r["period"]:r for r in rows}; audit=[]
    for raw in cross:
        period=f'{raw["period"][:4]}-{raw["period"][4:6]}'
        if period not in release: continue
        cpi=numeric(raw["cpi_yoy_raw"]); ppi=numeric(raw["ppi_yoy_raw"]); base=release[period]
        audit.append({"period":period,"release_cpi_yoy":f'{base["cpi_yoy"]:.2f}',"snapshot_cpi_yoy":f'{cpi:.2f}',"cpi_delta_pp":f'{cpi-base["cpi_yoy"]:.2f}',
          "release_ppi_yoy":f'{base["ppi_yoy"]:.2f}',"snapshot_ppi_yoy":f'{ppi:.2f}',"ppi_delta_pp":f'{ppi-base["ppi_yoy"]:.2f}',
          "snapshot_revision_marker":str(any(x in (raw["cpi_yoy_raw"]+raw["ppi_yoy_raw"]).lower() for x in ("r","p"))).lower()})
    audit_fields=list(audit[0]); write_csv(out/"revision-audit.csv",audit,audit_fields)
    latest=rows[-1]; current=[r for r in rows if r["period"].startswith("2026-")]
    means={m:round(statistics.fmean(r[m] for r in current),2) for m in METRICS}
    max_revision=max(audit,key=lambda r:max(abs(float(r["cpi_delta_pp"])),abs(float(r["ppi_delta_pp"]))))
    results={"schemaVersion":1,"reportNumber":"SHWRP-2026-039","version":"1.0","dataPeriod":{"start":"2025-01","end":"2026-07","months":19},
      "latest":latest,"unweightedMonthlyMeans2026JanJul":means,"revisionAudit":{"overlapMonths":len(audit),"maxAbsoluteCpiDeltaPp":round(max(abs(float(r["cpi_delta_pp"])) for r in audit),2),
      "maxAbsolutePpiDeltaPp":round(max(abs(float(r["ppi_delta_pp"])) for r in audit),2),"largestAnyDeltaPeriod":max_revision["period"]},
      "counterexamples":{"positiveCpiNegativePpiMonths":[r["period"] for r in rows if r["cpi_yoy"]>0 and r["ppi_yoy"]<0],
      "all2025CpiPositive":all(r["cpi_yoy"]>0 for r in rows if r["period"].startswith("2025-")),"ppiNegativeRun2025MayDecMonths":8},
      "interpretationBoundary":"同月年增率點差不是傳遞率；指數範圍、權數、幣別、基期與版本不同。"}
    (out/"analysis-results.json").write_text(json.dumps(results,ensure_ascii=False,indent=2)+"\n",encoding="utf-8")
    svg_line(figs/"figure-1-four-index-yoy.svg","臺灣四項物價指數首發年增率（2025-01至2026-07）",rows,
      [("cpi_yoy","CPI"),("ppi_yoy","PPI"),("ipi_usd_yoy","IPI（美元）"),("epi_usd_yoy","EPI（美元）")],-10,25)
    svg_line(figs/"figure-2-upstream-cpi-gaps.svg","相對CPI的同月年增率點差（不是傳遞率）",rows,
      [("ppi_minus_cpi_pp","PPI−CPI"),("ipi_minus_cpi_pp","IPI−CPI"),("epi_minus_cpi_pp","EPI−CPI")],-10,25)
    svg_bars(figs/"figure-3-revision-audit.svg",audit)
    print(json.dumps({"status":"PASSED","rows":len(rows),"overlap":len(audit),"latest":latest},ensure_ascii=False))
    return results

if __name__=="__main__":
    parser=argparse.ArgumentParser(); parser.add_argument("--output-root",type=Path,default=ROOT); args=parser.parse_args(); run(args.output_root.resolve())
