"""Which share count belongs in a DCF: the cover-page count or the diluted
weighted average, and how far apart they actually are.

This is the evidence behind /guides/shares-outstanding-or-diluted-weighted-average/.
It is kept in the repo and published so the claims on that page can be checked
rather than taken on trust. Re-run it and the JSON regenerates; if a number on
the page and a number in the JSON disagree, the JSON is right and the page is
out of date.

Two tags answer two different questions:
  * dei:EntityCommonStockSharesOutstanding -- a point-in-time count, taken from
    the filing's cover page, usually dated a few weeks after the fiscal year end.
  * us-gaap:WeightedAverageNumberOfDilutedSharesOutstanding -- an average across
    the fiscal year, already including the dilutive effect of options, RSUs and
    convertibles.

A DCF wants one of them deliberately. Mixing them up is silent: both numbers
are real, both are in the billions for a large filer, and nothing says so when
free cash flow gets divided by the wrong one.

For every company this asks SEC EDGAR's companyconcept endpoint for both tags
and records:
  * whether the dei tag has any annual (10-K/20-F/40-F) facts at all -- several
    filers never tag the cover-page count under this exact name,
  * the gap in days between the dei fact's "end" (the cover-page date) and the
    us-gaap fiscal year end for the same filing,
  * the diluted weighted average for the same fiscal year,
  * the percentage difference between the two counts.

Usage:  python research/shares-outstanding-tag-survey.py
Writes: research/shares-outstanding-tag-survey.json
"""
import io
import json
import os
import time
import urllib.error
import urllib.request

UA = "Fairvalue Radar (privacy@fairvalueradar.com)"
HERE = os.path.dirname(os.path.abspath(__file__))
ANNUAL = ("10-K", "20-F", "40-F")

COMPANIES = [
    ("AAPL", 320193), ("MSFT", 789019), ("NVDA", 1045810), ("AMZN", 1018724),
    ("GOOGL", 1652044), ("META", 1326801), ("TSLA", 1318605), ("JPM", 19617),
    ("XOM", 34088), ("JNJ", 200406), ("WMT", 104169), ("PG", 80424),
    ("KO", 21344), ("PFE", 78003), ("INTC", 50863), ("CVX", 93410),
    ("T", 732717), ("BA", 12927), ("GE", 40545), ("MRK", 310158),
]

COVER_TAG = ("dei", "EntityCommonStockSharesOutstanding")
DILUTED_TAG = ("us-gaap", "WeightedAverageNumberOfDilutedSharesOutstanding")


def fetch(url, tries=4):
    """A 404 is an answer, not a failure: it means the company has never used
    that tag under that namespace. Anything else is retried."""
    req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
    for attempt in range(tries):
        try:
            with urllib.request.urlopen(req, timeout=30) as r:
                return json.loads(r.read().decode("utf-8")), None
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return None, "404"
            time.sleep(1.5 * (attempt + 1))
        except Exception:
            time.sleep(1.5 * (attempt + 1))
    return None, "unreachable after %d tries" % tries


def annual_facts(payload, unit):
    rows = [f for f in (payload or {}).get("units", {}).get(unit, [])
            if f.get("form") in ANNUAL and f.get("end")]
    rows.sort(key=lambda f: (f["end"], f.get("accn", "")))
    return rows


def concept(cik, ns, tag, unit):
    time.sleep(0.2)
    payload, err = fetch("https://data.sec.gov/api/xbrl/companyconcept/CIK%010d/%s/%s.json"
                          % (cik, ns, tag))
    if err:
        return None, err
    return annual_facts(payload, unit), None


def days_between(a, b):
    from datetime import date
    da = date(*[int(x) for x in a.split("-")])
    db = date(*[int(x) for x in b.split("-")])
    return abs((da - db).days)


def main():
    out = {"readOn": time.strftime("%Y-%m-%d"), "source": "SEC EDGAR XBRL companyconcept",
           "companies": []}
    for ticker, cik in COMPANIES:
        rec = {"ticker": ticker, "cik": cik}

        cover_rows, cover_err = concept(cik, COVER_TAG[0], COVER_TAG[1], "shares")
        if cover_err == "404":
            rec["cover"] = {"state": "never used"}
        elif cover_err:
            rec["cover"] = {"state": "fetch failed", "error": cover_err}
        elif not cover_rows:
            rec["cover"] = {"state": "empty answer"}
        else:
            last = cover_rows[-1]
            rec["cover"] = {"state": "has facts", "annualFacts": len(cover_rows),
                            "end": last["end"], "val": last["val"]}

        dil_rows, dil_err = concept(cik, DILUTED_TAG[0], DILUTED_TAG[1], "shares")
        if dil_err == "404":
            rec["diluted"] = {"state": "never used"}
        elif dil_err:
            rec["diluted"] = {"state": "fetch failed", "error": dil_err}
        elif not dil_rows:
            rec["diluted"] = {"state": "empty answer"}
        else:
            last = dil_rows[-1]
            rec["diluted"] = {"state": "has facts", "annualFacts": len(dil_rows),
                               "end": last["end"], "val": last["val"]}

        if (rec["cover"].get("state") == "has facts"
                and rec["diluted"].get("state") == "has facts"):
            gap = days_between(rec["cover"]["end"], rec["diluted"]["end"])
            # A gap past about 14 months means the two "last" facts are not the
            # same fiscal year -- usually because the diluted tag was abandoned
            # years before the cover tag was. Computing a percentage from two
            # different years would be exactly the trap the FCF guide describes,
            # so it is flagged instead of quietly answered.
            rec["dateGapDays"] = gap
            if gap > 425:
                rec["staleMatch"] = True
                rec["pctDifference"] = None
            else:
                cover_v, dil_v = rec["cover"]["val"], rec["diluted"]["val"]
                rec["pctDifference"] = round((cover_v - dil_v) / dil_v * 100.0, 2)
        else:
            rec["dateGapDays"] = None
            rec["pctDifference"] = None

        out["companies"].append(rec)
        print("%-6s cover=%-18s diluted=%-18s gap=%-5s diff=%s%%" % (
            ticker,
            rec["cover"].get("val", rec["cover"]["state"]),
            rec["diluted"].get("val", rec["diluted"]["state"]),
            rec["dateGapDays"], rec["pctDifference"]))

    io.open(os.path.join(HERE, "shares-outstanding-tag-survey.json"), "w", encoding="utf-8").write(
        json.dumps(out, indent=1))
    print("\nwritten: research/shares-outstanding-tag-survey.json")


if __name__ == "__main__":
    main()
