#!/usr/bin/env python3
"""The Tempo: reference implementation (weekly dry run).

Computes AppDeen Archive's Tempo, the daily meter of how fast AI is moving,
from public signals, with no dependencies beyond the Python standard library.

    Tempo = 100 + 20 * sum(w_i * z_i), clipped to 40..200

where z_i is each signal's count this week as a z-score against its own
trailing 52-week mean and standard deviation (z clipped to -3..3 so no single
signal can dominate). 100 is an average week; each 20 points is one standard
deviation. The paper signal is also compared with the same week a year earlier
(half weight on the trailing baseline, half on the year-ago difference) to
soften arXiv's conference-deadline seasons.

Signals (weight):
  papers     0.15  arXiv submissions in cs.AI, cs.LG, cs.CL (arXiv API)
  models     0.20  Hugging Face daily-papers volume and Epoch notable models
  capability 0.20  change in the frontier METR 50% time horizon (log2)
  money      0.15  AI funding rounds and dollars announced (AI Funding feed)
  noise      0.15  share of the world's press mentioning AI (GDELT)
  builders   0.15  Hacker News points on AI stories (Algolia API)

Production (Mahdi's pipeline) runs this daily on a rolling 7-day window ending
yesterday. This script runs it on weekly bins so a first reading can be taken
in one go; a signal that cannot be reached is dropped and the remaining
weights are rescaled, and the output says so.

Usage:
    python3 -I tempo.py --cache DIR --out DIR [--weeks 64] [--end YYYY-MM-DD]
"""
from __future__ import annotations

import argparse
import csv
import datetime as dt
import io
import json
import math
import os
import re
import statistics
import sys
import time
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET

WEIGHTS = {
    "papers": 0.15,
    "models": 0.20,
    "capability": 0.20,
    "money": 0.15,
    "noise": 0.15,
    "builders": 0.15,
}
LABELS = {
    "papers": "Papers",
    "models": "Models",
    "capability": "Capability",
    "money": "Money",
    "noise": "Noise",
    "builders": "Builders",
}
LEVELS = [
    (70, "Largo", "A quiet week, the field is catching its breath."),
    (100, "Andante", "Walking pace."),
    (120, "Moderato", "An ordinary week in AI, which is still fast."),
    (150, "Allegro", "Faster than its own average."),
    (180, "Presto", "Hold on."),
    (10**9, "Prestissimo", "We have never seen a week like this."),
]
BASELINE_WEEKS = 52
Z_CLIP = 3.0
UA = "AppDeen-Archive-Tempo/0.1 (research.appdeen.ai; mr@appdeen.ai)"


# ----------------------------------------------------------------- helpers

def log(msg: str) -> None:
    print(f"[{dt.datetime.now().strftime('%H:%M:%S')}] {msg}", flush=True)


def fetch(url: str, timeout: int = 60) -> bytes:
    req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "*/*"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return r.read()


def cached_json(cache_dir: str, name: str, builder):
    path = os.path.join(cache_dir, name + ".json")
    if os.path.exists(path):
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    data = builder()
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f)
    return data


def week_bins(end: dt.date, n: int) -> list[tuple[dt.date, dt.date]]:
    """n bins of 7 days, oldest first, the last one ending on `end`."""
    bins = []
    for k in range(n - 1, -1, -1):
        b_end = end - dt.timedelta(days=7 * k)
        b_start = b_end - dt.timedelta(days=6)
        bins.append((b_start, b_end))
    return bins


def level_for(value: float) -> tuple[str, str]:
    for upper, name, line in LEVELS:
        if value < upper:
            return name, line
    return LEVELS[-1][1], LEVELS[-1][2]


def zscores(series: list[float | None], baseline: int = BASELINE_WEEKS):
    """z for each index using the trailing `baseline` values (None where short)."""
    out: list[float | None] = []
    for i, x in enumerate(series):
        hist = [v for v in series[max(0, i - baseline):i] if v is not None]
        if x is None or len(hist) < 8:
            out.append(None)
            continue
        mean = statistics.fmean(hist)
        sd = statistics.pstdev(hist)
        floor = max(abs(mean) * 0.05, 1e-9)
        sd = max(sd, floor)
        out.append(max(-Z_CLIP, min(Z_CLIP, (x - mean) / sd)))
    return out


def baseline_stats(series, i, baseline=BASELINE_WEEKS):
    hist = [v for v in series[max(0, i - baseline):i] if v is not None]
    if len(hist) < 8:
        return None, None
    return statistics.fmean(hist), statistics.pstdev(hist)


# ----------------------------------------------------------------- signals

def pull_arxiv(bins, cache_dir):
    """Submissions in cs.AI, cs.LG or cs.CL per bin, via the arXiv API."""
    def build():
        counts = {}
        for b_start, b_end in bins:
            q = (f"(cat:cs.AI OR cat:cs.LG OR cat:cs.CL) AND submittedDate:"
                 f"[{b_start:%Y%m%d}0000 TO {b_end:%Y%m%d}2359]")
            url = ("https://export.arxiv.org/api/query?" +
                   urllib.parse.urlencode({"search_query": q, "max_results": 1}))
            for attempt in range(3):
                try:
                    xml = fetch(url).decode("utf-8", "replace")
                    m = re.search(r"<opensearch:totalResults[^>]*>(\d+)<", xml)
                    counts[b_end.isoformat()] = int(m.group(1)) if m else None
                    break
                except Exception as e:  # noqa: BLE001
                    log(f"arxiv {b_end} attempt {attempt+1} failed: {e}")
                    time.sleep(5)
            time.sleep(3.2)  # arXiv asks for 3 s between requests
            log(f"arxiv {b_start}..{b_end}: {counts.get(b_end.isoformat())}")
        return counts
    return cached_json(cache_dir, "arxiv_weekly", build)


def pull_hf_daily(bins, cache_dir):
    """Hugging Face daily papers: papers featured and upvotes per day."""
    def build():
        per_day = {}
        d = bins[0][0]
        last = bins[-1][1]
        while d <= last:
            url = f"https://huggingface.co/api/daily_papers?date={d.isoformat()}&limit=100"
            try:
                items = json.loads(fetch(url, timeout=40).decode("utf-8"))
                per_day[d.isoformat()] = {
                    "papers": len(items),
                    "upvotes": sum(int((it.get("paper") or {}).get("upvotes") or 0) for it in items),
                }
            except Exception as e:  # noqa: BLE001
                log(f"hf {d} failed: {e}")
                per_day[d.isoformat()] = None
            d += dt.timedelta(days=1)
            time.sleep(0.25)
        log(f"hf daily papers: {len(per_day)} days")
        return per_day
    per_day = cached_json(cache_dir, "hf_daily", build)
    papers, upvotes = {}, {}
    for b_start, b_end in bins:
        d, p, u, ok = b_start, 0, 0, 0
        while d <= b_end:
            v = per_day.get(d.isoformat())
            if v:
                p += v["papers"]; u += v["upvotes"]; ok += 1
            d += dt.timedelta(days=1)
        papers[b_end.isoformat()] = p if ok else None
        upvotes[b_end.isoformat()] = u if ok else None
    return papers, upvotes


def pull_epoch(bins, cache_dir):
    """Notable models per bin from Epoch AI's models dataset (CC-BY)."""
    def build():
        raw = fetch("https://epoch.ai/data/all_ai_models.csv", timeout=120).decode("utf-8", "replace")
        rows = list(csv.DictReader(io.StringIO(raw)))
        cols = rows[0].keys() if rows else []
        date_col = next((c for c in cols if c.strip().lower() in ("publication date", "release date")), None)
        notable_col = next((c for c in cols if "notability" in c.lower()), None)
        out = []
        for r in rows:
            d = (r.get(date_col) or "").strip() if date_col else ""
            if not re.match(r"\d{4}-\d{2}-\d{2}", d):
                continue
            notable = bool((r.get(notable_col) or "").strip()) if notable_col else True
            out.append({"date": d[:10], "notable": notable, "model": r.get("Model") or r.get("System") or ""})
        log(f"epoch: {len(rows)} rows, date column {date_col!r}, notability column {notable_col!r}")
        return out
    models = cached_json(cache_dir, "epoch_models", build)
    counts = {}
    for b_start, b_end in bins:
        counts[b_end.isoformat()] = sum(
            1 for m in models if m["notable"] and b_start.isoformat() <= m["date"] <= b_end.isoformat())
    return counts


def pull_metr(bins, cache_dir):
    """Frontier METR 50% time horizon (minutes) at each bin end, from METR's file."""
    def build():
        txt = fetch("https://metr.org/assets/benchmark_results_1_1.yaml").decode("utf-8")
        # Tolerant line parser for the known layout: results -> model -> metrics/release_date.
        models, cur, in_p50 = [], None, False
        for line in txt.splitlines():
            m = re.match(r"^  (\S+):\s*$", line)
            if m and not line.startswith("    "):
                cur = {"name": m.group(1)}
                models.append(cur)
                in_p50 = False
                continue
            if cur is None:
                continue
            if re.match(r"^\s+p50_horizon_length:\s*$", line):
                in_p50 = True; continue
            if re.match(r"^\s+p80_horizon_length:\s*$", line):
                in_p50 = False; continue
            mm = re.match(r"^\s+estimate:\s*([\d.]+)", line)
            if mm and in_p50 and "p50" not in cur:
                cur["p50"] = float(mm.group(1))
            md = re.match(r"^\s+release_date:\s*(\d{4}-\d{2}-\d{2})", line)
            if md:
                cur["release_date"] = md.group(1)
        models = [m for m in models if "p50" in m and "release_date" in m]
        dbl = re.search(r"from_2023_on:[\s\S]*?point_estimate:\s*([\d.]+)", txt)
        log(f"metr: {len(models)} models, post-2023 doubling time {dbl.group(1) if dbl else '?'} days")
        return {"models": models, "doubling_days_post_2023": float(dbl.group(1)) if dbl else None}
    data = cached_json(cache_dir, "metr", build)
    frontier, records = {}, []
    prev = None
    for b_start, b_end in bins:
        eligible = [m for m in data["models"] if m["release_date"] <= b_end.isoformat()]
        f = max((m["p50"] for m in eligible), default=None)
        frontier[b_end.isoformat()] = f
        if prev and f and f > prev * 1.001:
            best = max(eligible, key=lambda m: m["p50"])
            records.append({"week_end": b_end.isoformat(), "model": best["name"],
                            "p50_minutes": best["p50"], "release_date": best["release_date"]})
        prev = f
    # Signal: log2 change of the frontier per bin (doublings per week).
    change, last = {}, None
    for b_start, b_end in bins:
        f = frontier[b_end.isoformat()]
        change[b_end.isoformat()] = (math.log2(f) - math.log2(last)) if (f and last) else (0.0 if f else None)
        last = f
    return change, frontier, records, data.get("doubling_days_post_2023")


def parse_amount(text: str) -> float | None:
    m = re.search(r"\$\s?([\d][\d,]*\.?\d*)\s?(B|M|K|billion|million|thousand)?", text, re.I)
    if not m:
        return None
    n = float(m.group(1).replace(",", ""))
    unit = (m.group(2) or "").lower()
    mult = {"b": 1e9, "billion": 1e9, "m": 1e6, "million": 1e6, "k": 1e3, "thousand": 1e3}.get(unit, 1)
    return n * mult


def pull_funding(bins, cache_dir):
    """Rounds and dollars per bin from the AI Funding RSS feed (aifunding.me)."""
    def build():
        xml = fetch("https://aifunding.me/feed", timeout=90)
        root = ET.fromstring(xml)
        deals = []
        for it in root.findall(".//item"):
            cat = (it.findtext("category") or "").strip()
            if cat in ("Insights", "Roundups", "Newsletters", "Insight", "Roundup", "Newsletter"):
                continue
            title = it.findtext("title") or ""
            if "raises" not in title.lower() and "$" not in title:
                continue
            try:
                pub = dt.datetime.strptime(it.findtext("pubDate") or "", "%a, %d %b %Y %H:%M:%S %Z").date()
            except ValueError:
                continue
            deals.append({"date": pub.isoformat(), "title": title, "usd": parse_amount(title), "stage": cat,
                          "link": it.findtext("link") or ""})
        log(f"aifunding: {len(deals)} deal items")
        return deals
    deals = cached_json(cache_dir, "aifunding_deals", build)
    rounds, dollars, mega = {}, {}, []
    for b_start, b_end in bins:
        week = [d for d in deals if b_start.isoformat() <= d["date"] <= b_end.isoformat()]
        rounds[b_end.isoformat()] = len(week)
        dollars[b_end.isoformat()] = sum(d["usd"] or 0 for d in week)
        mega += [d for d in week if (d["usd"] or 0) >= 10e9]
    return rounds, dollars, mega, deals


def pull_hn(bins, cache_dir):
    """Points on Hacker News stories matching 'AI' per bin (Algolia API)."""
    def build():
        out = {}
        for b_start, b_end in bins:
            a = int(dt.datetime.combine(b_start, dt.time.min, dt.timezone.utc).timestamp())
            b = int(dt.datetime.combine(b_end + dt.timedelta(days=1), dt.time.min, dt.timezone.utc).timestamp())
            url = ("https://hn.algolia.com/api/v1/search_by_date?" + urllib.parse.urlencode({
                "query": "AI", "tags": "story", "hitsPerPage": 1000,
                "numericFilters": f"created_at_i>={a},created_at_i<{b}"}))
            try:
                data = json.loads(fetch(url, timeout=40).decode("utf-8"))
                hits = data.get("hits", [])
                out[b_end.isoformat()] = {"stories": len(hits), "points": sum(int(h.get("points") or 0) for h in hits)}
            except Exception as e:  # noqa: BLE001
                log(f"hn {b_end} failed: {e}")
                out[b_end.isoformat()] = None
            time.sleep(0.3)
        log(f"hn: {len(out)} bins")
        return out
    data = cached_json(cache_dir, "hn_weekly", build)
    return {k: (v["points"] if v else None) for k, v in data.items()}


# ----------------------------------------------------------------- compute

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--cache", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--weeks", type=int, default=64)
    ap.add_argument("--end", default=(dt.date.today() - dt.timedelta(days=1)).isoformat(),
                    help="last day of the current bin (default: yesterday)")
    args = ap.parse_args()
    os.makedirs(args.cache, exist_ok=True)
    os.makedirs(args.out, exist_ok=True)
    end = dt.date.fromisoformat(args.end)
    bins = week_bins(end, args.weeks)
    keys = [b_end.isoformat() for _, b_end in bins]
    log(f"{len(bins)} weekly bins, {bins[0][0]} .. {bins[-1][1]}")

    raw: dict[str, list] = {}
    notes: dict[str, str] = {}
    availability: dict[str, bool] = {}

    # papers
    try:
        arx = pull_arxiv(bins, args.cache)
        raw["papers"] = [arx.get(k) for k in keys]
        availability["papers"] = True
        notes["papers"] = "arXiv submissions in cs.AI, cs.LG, cs.CL per 7 days (arXiv API totalResults); the last bin can be under-counted while arXiv announces."
    except Exception as e:  # noqa: BLE001
        log(f"papers unavailable: {e}"); availability["papers"] = False

    # models: HF daily papers (count) + Epoch notable models, z-averaged
    hf_papers = hf_up = epoch_counts = None
    try:
        hf_papers, hf_up = pull_hf_daily(bins, args.cache)
    except Exception as e:  # noqa: BLE001
        log(f"hf unavailable: {e}")
    try:
        epoch_counts = pull_epoch(bins, args.cache)
    except Exception as e:  # noqa: BLE001
        log(f"epoch unavailable: {e}")
    availability["models"] = bool(hf_papers or epoch_counts)
    if hf_papers:
        raw["models_hf_papers"] = [hf_papers.get(k) for k in keys]
        raw["models_hf_upvotes"] = [hf_up.get(k) for k in keys]
    if epoch_counts:
        raw["models_epoch_notable"] = [epoch_counts.get(k) for k in keys]
    notes["models"] = ("Average of two z-scores: papers featured on Hugging Face daily papers per 7 days, and notable models "
                       "with a publication date in the window in Epoch AI's dataset (Epoch lags by weeks, so the latest bins run low). "
                       "Upvotes are recorded but not scored: fresh papers have had less time to collect them.")

    # capability: METR frontier
    metr_change = metr_frontier = metr_records = None
    doubling = None
    try:
        metr_change, metr_frontier, metr_records, doubling = pull_metr(bins, args.cache)
        raw["capability"] = [metr_change.get(k) for k in keys]
        raw["capability_frontier_minutes"] = [metr_frontier.get(k) for k in keys]
        availability["capability"] = True
        notes["capability"] = ("log2 change per 7 days of the frontier METR 50% time horizon (Time Horizon 1.1 file); "
                               "a new record is a step, an ordinary week is 0. Artificial Analysis' Intelligence Index "
                               "joins in production (needs an API key).")
    except Exception as e:  # noqa: BLE001
        log(f"capability unavailable: {e}"); availability["capability"] = False

    # money
    mega = []
    try:
        rounds, dollars, mega, deals = pull_funding(bins, args.cache)
        raw["money"] = [rounds.get(k) for k in keys]
        raw["money_usd"] = [dollars.get(k) for k in keys]
        availability["money"] = True
        # coverage check: the feed may thin out for older weeks
        recent = [v for v in raw["money"][-12:] if v is not None]
        early = [v for v in raw["money"][:12] if v is not None]
        if recent and early and statistics.median(early) < 0.3 * statistics.median(recent):
            notes["money_coverage"] = (f"AI Funding feed coverage thins for older weeks (early median {statistics.median(early)} "
                                       f"rounds/week vs recent {statistics.median(recent)}); the baseline is biased low and this z runs high.")
        notes["money"] = "Funding rounds per 7 days from the AI Funding RSS feed (aifunding.me/feed); dollars recorded alongside, mega-rounds (>= $10B) become quakes."
    except Exception as e:  # noqa: BLE001
        log(f"money unavailable: {e}"); availability["money"] = False

    # noise: GDELT
    availability["noise"] = False
    notes["noise"] = "GDELT DOC API (share of world press mentioning AI) is not reachable from this sandbox; production computes it server-side."

    # builders
    try:
        hn = pull_hn(bins, args.cache)
        raw["builders"] = [hn.get(k) for k in keys]
        availability["builders"] = True
        notes["builders"] = "Points on Hacker News stories matching 'AI' per 7 days (Algolia search_by_date, up to 1,000 stories per bin)."
    except Exception as e:  # noqa: BLE001
        log(f"builders unavailable: {e}"); availability["builders"] = False

    # z-scores per signal
    z: dict[str, list] = {}
    if availability.get("papers"):
        zt = zscores(raw["papers"])
        # year-ago comparison, half weight, where a year-ago bin exists
        zp = []
        for i, x in enumerate(raw["papers"]):
            if zt[i] is None:
                zp.append(None); continue
            yoy = raw["papers"][i - 52] if i >= 52 else None
            mean, sd = baseline_stats(raw["papers"], i)
            if yoy and sd:
                sd = max(sd, abs(mean) * 0.05, 1e-9)
                zy = max(-Z_CLIP, min(Z_CLIP, (x - yoy) / sd))
                zp.append(0.5 * zt[i] + 0.5 * zy)
            else:
                zp.append(zt[i])
        z["papers"] = zp
    if availability.get("models"):
        parts = [zscores(raw[k]) for k in ("models_hf_papers", "models_epoch_notable") if k in raw]
        z["models"] = [None if any(p[i] is None for p in parts) else statistics.fmean(p[i] for p in parts)
                       for i in range(len(keys))]
    if availability.get("capability"):
        z["capability"] = zscores(raw["capability"])
    if availability.get("money"):
        z["money"] = zscores(raw["money"])
    if availability.get("builders"):
        z["builders"] = zscores(raw["builders"])

    used = [k for k in WEIGHTS if availability.get(k)]
    wsum = sum(WEIGHTS[k] for k in used)
    eff = {k: WEIGHTS[k] / wsum for k in used}

    series = []
    for i, k in enumerate(keys):
        if any(z[s][i] is None for s in used):
            series.append({"date": k, "value": None, "level": None})
            continue
        t = 100 + 20 * sum(eff[s] * z[s][i] for s in used)
        t = max(40, min(200, t))
        name, _ = level_for(t)
        series.append({"date": k, "value": round(t), "level": name})

    valid = [s for s in series if s["value"] is not None]
    if not valid:
        log("no bin had a full baseline; nothing to report"); return 1
    today = valid[-1]
    prev = valid[-2] if len(valid) > 1 else None
    i_today = keys.index(today["date"])
    name, line = level_for(today["value"])

    signals = []
    for s in WEIGHTS:
        if s in used:
            raw_key = {"models": "models_hf_papers"}.get(s, s)
            mean, sd = baseline_stats(raw[raw_key], i_today)
            signals.append({
                "key": s, "label": LABELS[s],
                "count": raw[raw_key][i_today],
                "baseline_mean": None if mean is None else round(mean, 2),
                "baseline_sd": None if sd is None else round(sd, 2),
                "z": round(z[s][i_today], 2),
                "weight": round(WEIGHTS[s], 2),
                "weight_used": round(eff[s], 3),
                "contribution": round(20 * eff[s] * z[s][i_today], 2),
                "source": notes.get(s, ""),
            })
        else:
            signals.append({"key": s, "label": LABELS[s], "count": None, "baseline_mean": None, "baseline_sd": None,
                            "z": None, "weight": round(WEIGHTS[s], 2), "weight_used": 0.0, "contribution": 0.0,
                            "unavailable": True, "source": notes.get(s, "")})

    movers = [s["key"] for s in sorted((s for s in signals if not s.get("unavailable")),
                                       key=lambda s: -abs(s["contribution"]))][:3]

    quakes = []
    cutoff = (end - dt.timedelta(days=90)).isoformat()
    for r in (metr_records or []):
        if r["week_end"] >= cutoff:
            quakes.append({"date": r["release_date"], "magnitude": 7, "kind": "metr-record", "record_number": None,
                           "title": f"New METR time-horizon record: {r['model']} at {r['p50_minutes']:.0f} min (50%)"})
    for d in mega:
        if d["date"] >= cutoff:
            quakes.append({"date": d["date"], "magnitude": 6, "kind": "mega-round", "record_number": None,
                           "title": d["title"], "url": d["link"]})
    quakes.sort(key=lambda q: q["date"])

    out = {
        "date": end.isoformat(),
        "computed_at": dt.datetime.now(dt.timezone(dt.timedelta(hours=3))).isoformat(timespec="seconds"),
        "value": today["value"],
        "level": name,
        "line": line,
        "delta_7d": (today["value"] - prev["value"]) if prev else None,
        "signals": signals,
        "movers": movers,
        "quakes": quakes,
        "series_weekly": valid[-13:],
        "method_url": "/archive/papers/bp-001",
        "dry_run": {
            "note": ("Reference run on weekly bins ending " + end.isoformat() + ". Signals unavailable from this "
                     "machine were dropped and the remaining weights rescaled; production runs daily on a rolling "
                     "7-day window with all six signals."),
            "signals_used": used,
            "weights_used": eff,
            "baseline_weeks": BASELINE_WEEKS,
            "z_clip": Z_CLIP,
            "metr_doubling_days_post_2023": doubling,
            "coverage_notes": {k: v for k, v in notes.items() if k.endswith("_coverage")},
        },
    }
    with open(os.path.join(args.out, "tempo.json"), "w", encoding="utf-8") as f:
        json.dump(out, f, indent=2)

    # per-week CSV of every raw count and z
    cols = ["week_end"] + sorted(raw) + [f"z_{s}" for s in used] + ["tempo", "level"]
    with open(os.path.join(args.out, "series.csv"), "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f); w.writerow(cols)
        for i, k in enumerate(keys):
            row = [k] + [raw[c][i] for c in sorted(raw)] + [None if z[s][i] is None else round(z[s][i], 3) for s in used]
            row += [series[i]["value"], series[i]["level"]]
            w.writerow(row)

    log(f"TEMPO {end}: {today['value']} {name} (delta {out['delta_7d']}), movers {movers}, quakes {len(quakes)}")
    for s in signals:
        log(f"  {s['key']:<11} count={s['count']!s:<10} z={s['z']!s:<6} w={s['weight_used']:<6} contrib={s['contribution']}"
            + ("  (unavailable)" if s.get("unavailable") else ""))
    return 0


if __name__ == "__main__":
    sys.exit(main())
