#!/usr/bin/env python3
"""Derive capacity_timeseries.csv and company_gw_chart.html from forecast_vintage.csv."""

from __future__ import annotations

import csv
from pathlib import Path

BASE = Path(__file__).resolve().parent
VINTAGE = BASE / "forecast_vintage.csv"
TIMESERIES = BASE / "capacity_timeseries.csv"
CHART = BASE / "company_gw_chart.html"

QUARTERS = [
    "FY2024-FQ4",
    "FY2025-FQ1",
    "FY2025-FQ2",
    "FY2025-FQ3",
    "FY2025-FQ4",
    "FY2026-FQ1",
    "FY2026-FQ2",
    "FY2026-FQ3",
]

# First quarter with FY2026 full-year capex guidance vintage (as-of FY2025-FQ4).
GUIDANCE_ORIGIN_QUARTER = "FY2025-FQ4"

FIELDS = [
    "company",
    "ticker",
    "fiscal_quarter",
    "actual_gw",
    "latest_forecast_gw",
    "original_forecast_gw",
    "under_construction_gw",
    "capex_actual_usd_b",
    "capex_guidance_latest_usd_b",
    "capex_guidance_original_usd_b",
    "capex_actual_metric",
    "capex_actual_is_derived",
    "caveat",
]


def period_key(p: str) -> tuple[int, int]:
    fy = int(p[2:6])
    fq = int(p.split("-FQ")[1])
    return (fy, fq)


def load_vintage() -> list[dict[str, str]]:
    with VINTAGE.open(encoding="utf-8") as f:
        return list(csv.DictReader(f))


def guidance_vintages(rows: list[dict[str, str]]) -> list[tuple[str, float]]:
    out: list[tuple[str, float]] = []
    for row in rows:
        if row["metric_name"] == "capex_guidance" and row["target_period"] == "FY2026":
            out.append((row["as_of_period"], float(row["raw_value"])))
    return sorted(out, key=lambda x: period_key(x[0]))


def fmt_usd_b(value: float) -> str:
    if value == int(value):
        return str(int(value))
    return f"{value:.3f}".rstrip("0").rstrip(".")


def guidance_as_of(quarter: str, vintages: list[tuple[str, float]]) -> str:
    """Return latest FY2026 guidance vintage available as-of quarter (blank if none yet)."""
    qk = period_key(quarter)
    latest = ""
    for period, value in vintages:
        if period_key(period) <= qk:
            latest = fmt_usd_b(value)
    return latest


def original_guidance_as_of(quarter: str, vintages: list[tuple[str, float]]) -> str:
    """Original FY2026 guide only visible from first guidance vintage quarter onward."""
    if period_key(quarter) < period_key(GUIDANCE_ORIGIN_QUARTER):
        return ""
    if not vintages:
        return ""
    return fmt_usd_b(vintages[0][1])


def capex_actual_for_quarter(quarter: str, rows: list[dict[str, str]]) -> tuple[str, str, str]:
    for row in rows:
        if (
            row["as_of_period"] == quarter
            and row["metric_name"] == "capex_actual"
            and row["target_period"] == quarter
        ):
            return row["raw_value"], "capex_actual", row["is_derived"]

    if quarter == "FY2024-FQ4":
        for row in rows:
            if (
                row["as_of_period"] == quarter
                and row["metric_name"] == "capex_actual"
                and row["target_period"] == "FY2024"
            ):
                return row["raw_value"], "capex_actual_fy", row["is_derived"]

    if quarter == "FY2025-FQ4":
        for row in rows:
            if (
                row["as_of_period"] == quarter
                and row["metric_name"] == "capex_actual"
                and row["target_period"] == "FY2025-FQ4"
            ):
                return row["raw_value"], "capex_actual", row["is_derived"]

    return "", "", ""


def build_timeseries(rows: list[dict[str, str]]) -> list[dict[str, str]]:
    guidance = guidance_vintages(rows)
    caveat = (
        "Microsoft does not disclose data-center MW/GW; GW columns blank. "
        "capex_*_usd_b are USD billions proxies from SEC/IR primary sources. "
        "FY2024-FQ4 capex_actual is full-year FY2024 (not quarterly-comparable). "
        "FY2026 guidance vintages apply only from FY2025-FQ4 onward."
    )
    out: list[dict[str, str]] = []
    for quarter in QUARTERS:
        capex, metric, derived = capex_actual_for_quarter(quarter, rows)
        out.append(
            {
                "company": "Microsoft",
                "ticker": "MSFT",
                "fiscal_quarter": quarter,
                "actual_gw": "",
                "latest_forecast_gw": "",
                "original_forecast_gw": "",
                "under_construction_gw": "",
                "capex_actual_usd_b": capex,
                "capex_guidance_latest_usd_b": guidance_as_of(quarter, guidance),
                "capex_guidance_original_usd_b": original_guidance_as_of(quarter, guidance),
                "capex_actual_metric": metric,
                "capex_actual_is_derived": derived,
                "caveat": caveat,
            }
        )
    return out


def svg_chart(rows: list[dict[str, str]]) -> str:
    labels = [r["fiscal_quarter"] for r in rows]
    raw_actual = [float(r["capex_actual_usd_b"]) if r["capex_actual_usd_b"] else None for r in rows]
    latest = [float(r["capex_guidance_latest_usd_b"]) if r["capex_guidance_latest_usd_b"] else None for r in rows]
    original = [
        float(r["capex_guidance_original_usd_b"]) if r["capex_guidance_original_usd_b"] else None
        for r in rows
    ]

    # Exclude FY2024 annual anchor from quarterly-comparable blue polyline.
    quarterly_actual = [None if r["capex_actual_metric"] == "capex_actual_fy" else v for r, v in zip(rows, raw_actual)]
    annual_anchor_idx = next(
        (i for i, r in enumerate(rows) if r["capex_actual_metric"] == "capex_actual_fy"),
        None,
    )

    w, h, ml, mr, mt, mb = 920, 480, 70, 30, 70, 90
    pw, ph = w - ml - mr, h - mt - mb
    all_vals = [v for v in quarterly_actual + latest + original if v is not None]
    if annual_anchor_idx is not None and raw_actual[annual_anchor_idx] is not None:
        all_vals.append(raw_actual[annual_anchor_idx])
    ymax = max(all_vals) * 1.1 if all_vals else 100
    ymin = 0

    def x(i: int) -> float:
        return ml + (i / max(len(labels) - 1, 1)) * pw

    def y(v: float) -> float:
        return mt + ph - ((v - ymin) / (ymax - ymin)) * ph

    def polyline(vals: list[float | None], color: str) -> str:
        pts = []
        for i, v in enumerate(vals):
            if v is not None:
                pts.append(f"{x(i):.1f},{y(v):.1f}")
        if len(pts) < 2:
            return ""
        return f'<polyline fill="none" stroke="{color}" stroke-width="2.5" points="{" ".join(pts)}"/>'

    annual_marker = ""
    if annual_anchor_idx is not None and raw_actual[annual_anchor_idx] is not None:
        ax, ay = x(annual_anchor_idx), y(raw_actual[annual_anchor_idx])
        annual_marker = (
            f'<polygon points="{ax:.1f},{ay-6:.1f} {ax+5:.1f},{ay:.1f} {ax:.1f},{ay+6:.1f} '
            f'{ax-5:.1f},{ay:.1f}" fill="#1d4ed8" stroke="#1e3a8a" stroke-width="1"/>'
            f'<text x="{ax:.1f}" y="{ay-14:.1f}" text-anchor="middle" font-size="9" fill="#1e3a8a">'
            f"FY2024 annual</text>"
        )

    grid = []
    step = 10 if ymax <= 100 else 20
    for tick in range(0, int(ymax) + 1, step):
        yy = y(tick)
        grid.append(f'<line x1="{ml}" y1="{yy:.1f}" x2="{w-mr}" y2="{yy:.1f}" stroke="#e5e7eb"/>')
        grid.append(
            f'<text x="{ml-8}" y="{yy+4:.1f}" text-anchor="end" font-size="11" fill="#6b7280">{tick}</text>'
        )

    xlabels = "".join(
        f'<text x="{x(i):.1f}" y="{h-50}" text-anchor="end" font-size="10" fill="#374151" '
        f'transform="rotate(-35 {x(i):.1f} {h-50})">{lab}</text>'
        for i, lab in enumerate(labels)
    )

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>MSFT — Capex Actual vs FY2026 Guidance Vintages (GW Proxy)</title>
  <style>
    body {{ font-family: system-ui, sans-serif; margin: 24px; background: #fafafa; color: #111; }}
    .banner {{ background: #fef3c7; border: 1px solid #f59e0b; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }}
    h1 {{ font-size: 20px; margin: 0 0 8px; }}
    .legend {{ display: flex; gap: 20px; margin-top: 12px; font-size: 13px; flex-wrap: wrap; }}
    .swatch {{ display: inline-block; width: 14px; height: 3px; margin-right: 6px; vertical-align: middle; }}
    .diamond {{ display: inline-block; width: 10px; height: 10px; background: #1d4ed8; transform: rotate(45deg); margin-right: 6px; vertical-align: middle; }}
  </style>
</head>
<body>
  <div class="banner">
    <strong>GW-proxy caveat:</strong> Microsoft does not disclose data-center MW/GW capacity.
    This chart plots <em>USD billions</em> quarterly capex actuals (SEC cash-flow additions to PPE)
    vs implied full-year FY2026 capex guidance vintages applied <em>as-of</em> each quarter
    (original ~$77.5B from FY2025-FQ4; latest ~$96.8B from FY2026-FQ1).
    FY2024-FQ4 is shown as a separate <strong>annual</strong> anchor (diamond), not on the quarterly line.
    Not convertible to GW without undisclosed assumptions.
  </div>
  <h1>Microsoft (MSFT) — Infrastructure Spend Proxy: Actual vs Guidance</h1>
  <svg width="{w}" height="{h}" viewBox="0 0 {w} {h}" role="img" aria-label="MSFT capex chart">
    <rect x="{ml}" y="{mt}" width="{pw}" height="{ph}" fill="#fff" stroke="#d1d5db"/>
    {''.join(grid)}
    <text x="{ml + pw/2:.0f}" y="{h-12}" text-anchor="middle" font-size="12" fill="#374151">Fiscal quarter (as-of)</text>
    <text transform="translate(18 {mt + ph/2:.0f}) rotate(-90)" text-anchor="middle" font-size="12" fill="#374151">USD billions (capex proxy)</text>
    {polyline(quarterly_actual, '#2563eb')}
    {polyline(latest, '#16a34a')}
    {polyline(original, '#dc2626')}
    {annual_marker}
    {xlabels}
  </svg>
  <div class="legend">
    <span><span class="swatch" style="background:#2563eb"></span>Capex actual (quarterly proxy)</span>
    <span><span class="diamond"></span>FY2024 full-year actual (annual anchor)</span>
    <span><span class="swatch" style="background:#16a34a"></span>Latest FY2026 capex guidance vintage (as-of)</span>
    <span><span class="swatch" style="background:#dc2626"></span>Original FY2026 capex guidance (~$77.5B, from FY2025-FQ4)</span>
  </div>
</body>
</html>
"""


def main() -> None:
    vintage = load_vintage()
    ts = build_timeseries(vintage)
    with TIMESERIES.open("w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows(ts)
    CHART.write_text(svg_chart(ts), encoding="utf-8")
    print(f"Wrote {len(ts)} rows to {TIMESERIES}")
    print(f"Wrote chart to {CHART}")


if __name__ == "__main__":
    main()
