"""What happens to a DCF's terminal value when the growth rate you picked is
not safely below the discount rate you picked.

This is the evidence behind /guides/terminal-growth-above-discount-rate/. 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.

The Gordon growth terminal value formula is

    TV = FCF * (1 + g) / (r - g)

and it is only defined for r > g. It does not fail loudly when that stops being
true. For g just under r the multiplier 1/(r-g) explodes to absurd multiples
with no error; for g >= r the formula returns a negative number that still
looks like an ordinary output. Nothing in the arithmetic itself objects.

A natural way to pick "the growth rate" without thinking about it is to take a
company's own trailing growth and carry it straight into the terminal year.
This script does exactly that, live, for the same 20 large filers surveyed in
the first two guides on this site: it pulls each company's last five fiscal
years of revenue and free cash flow (operating cash flow minus capex, the same
subtraction the free-cash-flow guide covers) from SEC EDGAR, computes the
5-year CAGR of each, and checks that CAGR against one fixed discount rate --
9%, the middle of the range most default DCF templates use for a diversified
large-cap, and the value this site's own calculator falls back to before a
user changes it. A company-specific WACC needs a market cap and a beta, which
is not something SEC EDGAR carries, so the comparison here is deliberately a
single stated constant, not a per-company estimate. What is live is the growth
rate on the other side of the comparison.

Usage:  python research/terminal-growth-survey.py
Writes: research/terminal-growth-survey.json
"""
import io
import json
import os
import time
import urllib.error
import urllib.request
from datetime import datetime

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

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),
]

REVENUE_TAGS = ["RevenueFromContractWithCustomerExcludingAssessedTax",
                "RevenueFromContractWithCustomerIncludingAssessedTax",
                "Revenues", "SalesRevenueNet"]
OCF_TAGS = ["NetCashProvidedByUsedInOperatingActivities",
            "NetCashProvidedByUsedInOperatingActivitiesContinuingOperations"]
CAPEX_TAGS = ["PaymentsToAcquirePropertyPlantAndEquipment",
              "PaymentsToAcquireProductiveAssets"]


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_series(payload):
    """One fact per annual, full-year period, newest last. A quarterly or
    cumulative-quarter fact under the same tag is dropped by its duration,
    the same guard build-stock-pages.py uses."""
    units = (payload or {}).get("units", {})
    rows = units.get("USD") or next(iter(units.values()), [])
    by = {}
    for f in rows:
        if f.get("form") not in ANNUAL or not f.get("end") or not f.get("start"):
            continue
        days = (datetime.fromisoformat(f["end"]) - datetime.fromisoformat(f["start"])).days
        if days < 300 or days > 400:
            continue
        prev = by.get(f["end"])
        if not prev or f.get("accn", "") > prev.get("accn", ""):
            by[f["end"]] = f
    return [by[k] for k in sorted(by)]


def pick_series(cik, tags):
    """Among candidate tags, keep the one whose newest annual fact is newest --
    an abandoned tag still answers, so the first hit is not necessarily live."""
    best, best_tag = [], None
    for tag in tags:
        time.sleep(0.2)
        payload, err = fetch("https://data.sec.gov/api/xbrl/companyconcept/CIK%010d/us-gaap/%s.json"
                              % (cik, tag))
        if err:
            continue
        series = annual_series(payload)
        if series and (not best or series[-1]["end"] > best[-1]["end"]):
            best, best_tag = series, tag
    return best, best_tag


def cagr(series, years):
    if len(series) < years + 1:
        return None
    a, b = series[-1 - years]["val"], series[-1]["val"]
    if a <= 0 or b <= 0:
        return None
    return round((b / a) ** (1.0 / years) - 1, 4)


def main():
    out = {"readOn": time.strftime("%Y-%m-%d"),
           "source": "SEC EDGAR XBRL companyconcept (revenue, operating cash flow, capex)",
           "discountRateAssumption": DISCOUNT_RATE,
           "note": "discountRateAssumption is a single stated constant, not a per-company WACC -- "
                   "SEC EDGAR has no market cap or beta to compute one. The CAGRs are live.",
           "companies": []}

    for ticker, cik in COMPANIES:
        rec = {"ticker": ticker, "cik": cik}

        rev_series, rev_tag = pick_series(cik, REVENUE_TAGS)
        ocf_series, ocf_tag = pick_series(cik, OCF_TAGS)
        capex_series, capex_tag = pick_series(cik, CAPEX_TAGS)

        fcf_series = []
        if ocf_series and capex_series:
            capex_by_end = {f["end"]: f["val"] for f in capex_series}
            for f in ocf_series:
                if f["end"] in capex_by_end:
                    fcf_series.append({"end": f["end"], "val": f["val"] - capex_by_end[f["end"]]})

        rev_cagr5 = cagr(rev_series, 5)
        fcf_cagr5 = cagr(fcf_series, 5)

        rec["revenue"] = {"tag": rev_tag, "annualFacts": len(rev_series),
                           "cagr5": rev_cagr5} if rev_series else {"state": "no annual facts"}
        rec["fcf"] = {"ocfTag": ocf_tag, "capexTag": capex_tag, "annualFacts": len(fcf_series),
                      "cagr5": fcf_cagr5} if fcf_series else {"state": "no annual FCF series"}

        naive_g = fcf_cagr5 if fcf_cagr5 is not None else rev_cagr5
        rec["naiveTerminalGrowth"] = naive_g
        rec["naiveGrowthSource"] = "fcf" if fcf_cagr5 is not None else ("revenue" if rev_cagr5 is not None else None)

        if naive_g is None:
            rec["verdict"] = "insufficient data"
            rec["terminalValueMultiplier"] = None
        elif naive_g >= DISCOUNT_RATE:
            rec["verdict"] = "breaks the formula: g >= r, terminal value is negative"
            rec["terminalValueMultiplier"] = None
        else:
            multiplier = (1 + naive_g) / (DISCOUNT_RATE - naive_g)
            rec["terminalValueMultiplier"] = round(multiplier, 1)
            rec["verdict"] = ("near-break: multiplier above 30x a single year's FCF"
                               if multiplier > 30 else "within range")

        out["companies"].append(rec)
        print("%-6s naive_g=%-8s source=%-9s multiplier=%-7s verdict=%s" % (
            ticker, naive_g, rec["naiveGrowthSource"], rec["terminalValueMultiplier"], rec["verdict"]))

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


if __name__ == "__main__":
    main()
