"""
Large US public-company bankruptcies, monthly, April 1980 to date.

A case is a Chapter 7 or Chapter 11 petition by a US public company with total
assets of at least $100 million in 1980 dollars. This is the definition used by
the Florida-UCLA-LoPucki Bankruptcy Research Database (BRD).

  1980-04 to 2022-12  Monthly counts from the BRD (final release, January 2023),
                      written at the bottom of this file. The BRD itself is
                      licensed and is not redistributed here, only these counts.
  2023-01 onwards     The BRD was discontinued, so cases come from SEC Form 8-K
                      filings under Item 1.03 (Bankruptcy or Receivership),
                      screened with the same size rule using XBRL total assets
                      and read one by one. They are listed in
                      bankruptcy_cases.csv with a link to each filing.

Usage
  python update_bankruptcies.py              build bankruptcies_monthly.csv
  python update_bankruptcies.py --harvest    search EDGAR for new Item 1.03
                                             filings and write candidates_to_review.csv

The harvest needs a User-Agent that identifies you, as SEC asks of automated
users. Set it once, for example in PowerShell:
  $env:SEC_USER_AGENT = "Your Name your.name@university.ac.uk"

Reviewing candidates (the step that cannot be automated)
  Open each filing_url in candidates_to_review.csv and read Item 1.03.
  * If the registrant itself filed a Chapter 7 or 11 petition, add a row to
    bankruptcy_cases.csv with the petition date stated in the text.
  * If not (only a subsidiary filed, or the filing reports a plan, an
    emergence or a case already counted), add the accession number and the
    reason to not_cases.csv so it is not proposed again.
  Then run the script again without --harvest.

Output columns (bankruptcies_monthly.csv)
  month           YYYY-MM
  cases           large public-company bankruptcy petitions in the month
  cases_ch11      of which Chapter 11
  cases_ch7       of which Chapter 7 (the BRD has 3 cases that are neither)
  cases_ma12      trailing 12-month mean of cases
  source          brd or sec_8k
  nber_recession  NBER recession indicator, FRED USREC

The month is the petition month. A month is only included once the four-day
8-K deadline has passed for every petition in it.

Cite: LoPucki, L. M. (2023), Florida-UCLA-LoPucki Bankruptcy Research
Database, final release January 2023, for 1980-2022.

Requires: Python 3.9+, pandas.
"""

import argparse
import datetime as dt
import html
import json
import os
import re
import sys
import time
from io import StringIO
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

import pandas as pd

HERE = Path(__file__).resolve().parent
CASES = HERE / "bankruptcy_cases.csv"
NOT_CASES = HERE / "not_cases.csv"
STATE = HERE / "harvest_state.json"
SPLICE = pd.Period("2023-01", freq="M")  # first month counted from SEC filings
FRED_CSV = "https://fred.stlouisfed.org/graph/fredgraph.csv?id={}&cosd=1979-01-01"
SEC_USER_AGENT = os.environ.get("SEC_USER_AGENT", "").strip()


# ---------------------------------------------------------------- build

def fred(series_id: str) -> pd.Series:
    req = Request(FRED_CSV.format(series_id), headers={"User-Agent": "bankruptcy-update/1.0"})
    with urlopen(req, timeout=60) as resp:
        df = pd.read_csv(StringIO(resp.read().decode("utf-8")), na_values=["."])
    df.columns = ["date", series_id]
    df["month"] = pd.to_datetime(df["date"]).dt.to_period("M")
    return df.set_index("month")[series_id].astype(float)


def brd_counts() -> pd.DataFrame:
    """BRD monthly counts, 1980-04 to 2022-12."""
    out = {}
    for col, block in (("cases", BRD_ALL), ("cases_ch11", BRD_CH11), ("cases_ch7", BRD_CH7)):
        vals = {}
        for line in block.strip().splitlines():
            year, *months = line.split()
            for m, v in enumerate(months, start=1):
                if v != "-":
                    vals[pd.Period(f"{year}-{m:02d}", freq="M")] = int(v)
        out[col] = pd.Series(vals)
    return pd.DataFrame(out)


def last_complete_month(today: dt.date) -> pd.Period:
    """8-Ks are due four business days after a petition, so a month is complete about a week after it ends."""
    prev = pd.Period(today, freq="M") - 1
    return prev if today.day >= 8 else prev - 1


def build(end: pd.Period) -> pd.DataFrame:
    brd = brd_counts()
    cases = pd.read_csv(CASES, parse_dates=["petition_date"])
    cases["month"] = cases["petition_date"].dt.to_period("M")
    months = pd.period_range("1980-04", end, freq="M")
    early = months < SPLICE

    def count(rows):
        return rows.groupby("month").size().reindex(months, fill_value=0)

    sec = pd.DataFrame({"cases": count(cases),
                        "cases_ch11": count(cases[cases["chapter"] == 11]),
                        "cases_ch7": count(cases[cases["chapter"] == 7])})
    df = pd.DataFrame(index=months)
    for col in ("cases", "cases_ch11", "cases_ch7"):
        df[col] = brd[col].reindex(months).where(early, sec[col]).astype(int)
    df["cases_ma12"] = df["cases"].rolling(12, min_periods=1).mean().round(4)
    df["source"] = ["brd" if e else "sec_8k" for e in early]
    df["nber_recession"] = fred("USREC").reindex(months).astype("Int64")
    df.index = df.index.strftime("%Y-%m")
    df.index.name = "month"
    return df


# ---------------------------------------------------------------- harvest

def sec_get(url: str, as_json: bool = True):
    if not SEC_USER_AGENT or "@" not in SEC_USER_AGENT:
        sys.exit('Set SEC_USER_AGENT to "Your Name your@email" before running --harvest (SEC fair-access policy).')
    req = Request(url, headers={"User-Agent": SEC_USER_AGENT})
    for attempt in range(4):
        try:
            with urlopen(req, timeout=60) as resp:
                data = resp.read()
            time.sleep(0.15)  # well under SEC's limit of ten requests a second
            return json.loads(data) if as_json else data.decode("utf-8", "ignore")
        except HTTPError as err:
            if err.code == 404:
                return None
            if err.code in (429, 500, 502, 503) and attempt < 3:
                time.sleep(3 * (attempt + 1))
                continue
            raise


def search_8k(start: str, end: str) -> dict:
    """Main 8-K documents whose items include 1.03, from two full-text queries."""
    found = {}
    for phrase in ("%22Item%201.03%22", "%22Bankruptcy%20or%20Receivership%22"):
        offset = 0
        while True:
            url = (f"https://efts.sec.gov/LATEST/search-index?q={phrase}&forms=8-K"
                   f"&dateRange=custom&startdt={start}&enddt={end}" + (f"&from={offset}" if offset else ""))
            page = sec_get(url)["hits"]
            for h in page["hits"]:
                s = h["_source"]
                if s.get("file_type") == "8-K" and "1.03" in (s.get("items") or []):
                    found[s["adsh"]] = (h["_id"].split(":")[1], s)
            offset += len(page["hits"])
            if not page["hits"] or offset >= page["total"]["value"]:
                break
    return found


def cpi_annual() -> pd.Series:
    cpi = fred("CPIAUCNS")
    return cpi.groupby(cpi.index.year).mean()  # the current year is a partial-year average


def max_assets(cik: int, event: pd.Timestamp):
    """Largest us-gaap:Assets reported for a period ending in the 15 months before the event."""
    cc = sec_get(f"https://data.sec.gov/api/xbrl/companyconcept/CIK{cik:010d}/us-gaap/Assets.json")
    if not cc:
        return None
    lo = event - pd.DateOffset(months=15)
    vals = [u["val"] for u in cc["units"].get("USD", []) if lo <= pd.Timestamp(u["end"]) <= event]
    return max(vals) if vals else None


def item_103(url: str) -> str:
    raw = sec_get(url, as_json=False) or ""
    raw = re.sub(r"(?is)<(script|style).*?</\1>", " ", raw)
    text = re.sub(r"\s+", " ", html.unescape(re.sub(r"<[^>]+>", " ", raw)).replace("\xa0", " "))
    starts = [m.start() for m in re.finditer(r"item\s*1\.03", text, re.I)]
    heads = [i for i in starts if re.match(r"item\s*1\.03\.?\s*bankruptcy", text[i:i + 40], re.I)]
    if not starts:
        return ""
    i = (heads or starts)[0]
    nxt = re.search(r"item\s*\d\.\d\d", text[i + 10:], re.I)
    return text[i: i + 10 + nxt.start()] if nxt else text[i: i + 4000]


PETITION = re.compile(r"\b(filed|file|commenced)\b[^.]{0,120}?\b(petitions?|cases?)\b"
                      r"|filed for (relief|liquidation|protection)", re.I)
FOLLOW_UP = re.compile(r"confirm|effective date|emerge|as previously (disclosed|reported)|dismiss|convert", re.I)
DATE = re.compile(r"\b(January|February|March|April|May|June|July|August|September|October|November|December)"
                  r"\s+\d{1,2},\s+\d{4}")


def classify(section: str) -> dict:
    """Suggestions for the reviewer, read from the Item 1.03 text. Not decisions."""
    pet = PETITION.search(section)
    date = chapter = None
    defined = re.search("(" + DATE.pattern + r")\W{0,4}\(the\W{0,3}Petition Date", section)
    if pet:
        before = list(DATE.finditer(section[max(0, pet.start() - 800): pet.start()]))
        after = DATE.search(section[pet.end(): pet.end() + 200])
        found = before[-1] if before else after
        date = defined.group(1) if defined else (found.group(0) if found else None)
        chapter = re.search(r"chapter\s+(7|11|15)\b", section[pet.start(): pet.end() + 400], re.I)
    chapter = chapter or re.search(r"chapter\s+(7|11|15)\b", section, re.I)
    return {"petition_language": bool(pet),
            "petition_date_suggested": pd.Timestamp(date).date() if date else None,
            "chapter_suggested": int(chapter.group(1)) if chapter else None,
            "follow_up_language": bool(FOLLOW_UP.search(section))}


def harvest(start: str, end: str) -> None:
    cases = pd.read_csv(CASES, parse_dates=["petition_date"])
    seen = set(cases["accession"]) | set(pd.read_csv(NOT_CASES)["accession"])
    cpi = cpi_annual()
    hits = search_8k(start, end)
    rows, small, no_xbrl = [], 0, []
    for adsh, (filename, s) in sorted(hits.items(), key=lambda kv: kv[1][1]["file_date"]):
        if adsh in seen:
            continue
        cik = int(s["ciks"][0])
        name = re.sub(r"\s*\(.*$", "", s["display_names"][0]).strip()
        event = pd.Timestamp(s["period_ending"]) if (s.get("period_ending") or "") >= start[:4] else pd.Timestamp(s["file_date"])
        threshold = 100e6 * cpi.get(event.year, cpi.iloc[-1]) / 82.4
        assets = max_assets(cik, event)
        if assets is None:  # no balance sheet in the window: excluded by the size rule
            no_xbrl.append(f"{s['file_date']} {name}")
            continue
        if assets < threshold:
            small += 1
            continue
        url = f"https://www.sec.gov/Archives/edgar/data/{cik}/{adsh.replace('-', '')}/{filename}"
        prior = cases[(cases["cik"] == cik) & (cases["petition_date"] > event - pd.DateOffset(months=36))]
        rows.append({"file_date": s["file_date"], "event_date": event.date(), "name": name, "cik": cik,
                     "assets_usd": assets, "threshold_usd": round(threshold),
                     "counted_case_in_last_36m": not prior.empty,
                     **classify(item_103(url)), "accession": adsh, "filing_url": url})
    out = pd.DataFrame(rows)
    out.to_csv(HERE / "candidates_to_review.csv", index=False)
    STATE.write_text(json.dumps({"searched_through": end}, indent=2))
    print(f"Searched {start} to {end}: {len(hits)} Item 1.03 filings, {len(hits) - len(rows) - small - len(no_xbrl)} "
          f"already reviewed, {small} below the size threshold, {len(no_xbrl)} with no XBRL total assets, "
          f"{len(out)} to review in candidates_to_review.csv")
    if no_xbrl:
        print("No XBRL balance sheet in the 15 months before the event (excluded by the size rule):\n  " + "\n  ".join(no_xbrl))
    if len(out):
        print(out[["file_date", "name", "assets_usd", "petition_date_suggested", "chapter_suggested",
                   "follow_up_language", "counted_case_in_last_36m"]].to_string(index=False))


def main() -> None:
    ap = argparse.ArgumentParser(description="Large US public-company bankruptcies, monthly.")
    ap.add_argument("--harvest", action="store_true", help="search EDGAR for new Item 1.03 filings")
    ap.add_argument("--since", help="harvest start date, YYYY-MM-DD (default: 10 days before the last search)")
    ap.add_argument("--to", help="last month to include in the build, YYYY-MM (default: last complete month)")
    args = ap.parse_args()
    today = dt.date.today()
    if args.harvest:
        last = json.loads(STATE.read_text())["searched_through"] if STATE.exists() else "2023-01-01"
        start = args.since or (pd.Timestamp(last) - pd.Timedelta(days=10)).strftime("%Y-%m-%d")
        harvest(start, today.isoformat())
        return
    end = pd.Period(args.to, freq="M") if args.to else last_complete_month(today)
    df = build(end)
    out = HERE / "bankruptcies_monthly.csv"
    df.to_csv(out, float_format="%.10g")
    print(f"Wrote {out.name}: {len(df)} months, {df.index[0]} to {df.index[-1]}, {df['cases'].sum():,} cases")
    print(f"Last 12 months: {df['cases'].iloc[-12:].sum()} cases; latest month {df.index[-1]}: {df['cases'].iloc[-1]}")


# Florida-UCLA-LoPucki Bankruptcy Research Database, final release (12 January 2023):
# number of cases by month of filing. One row per year, January to December; "-" is
# before the series starts. All cases, then Chapter 11, then Chapter 7.
BRD_ALL = """
1980  -  -  -  1  0  0  0  0  1  1  0  0
1981  1  1  0  0  0  0  0  0  1  0  1  1
1982  0  1  0  3  1  1  2  2  0  1  1  1
1983  0  0  1  1  0  0  0  1  2  0  1  0
1984  0  0  0  1  0  0  1  0  1  2  0  1
1985  0  0  2  1  0  0  0  2  1  1  1  0
1986  2  1  2  0  0  0  1  1  1  1  1  1
1987  0  1  1  2  0  1  0  2  0  0  0  1
1988  1  4  1  0  1  0  3  2  0  0  0  0
1989  2  0  3  1  0  0  1  1  2  1  3  2
1990  2  2  4  5  2  2  1  0  2  4  3  3
1991  6  6  1  4  5  2  3  1  3  2  2  6
1992  8  2  2  1  2  3  0  4  2  7  1  0
1993  2  2  3  2  3  1  1  1  2  1  3  4
1994  2  2  1  0  0  0  0  1  1  0  4  1
1995  1  0  0  0  3  3  2  2  2  3  2  0
1996  3  0  0  1  1  1  2  2  3  2  0  1
1997  2  1  0  0  1  1  5  3  2  0  1  1
1998  5  3  0  0  1  2  3  1  4  4  4  4
1999  3  4  7  1  1  5  2  3  6  5  3  5
2000  7  6  4  8  9  9  4  7  7  5  6  8
2001  9  3  9 15  8  5  9  6  6  8  9 10
2002  9 13 10  7  8  7  2  3  3  7  8  6
2003  3  2  7  4  8  9  7  6  4  4  1  2
2004  1  4  7  1  2  3  0  1  3  3  2  2
2005  6  3  1  1  2  0  1  1  4  3  1  2
2006  1  1  3  0  1  1  1  1  2  2  0  1
2007  0  0  1  1  1  1  1  2  1  2  2  1
2008  3  3  2  3  1  1  4  2  3  4  6  7
2009 10  9 16  9  9  6  5  6  3  6 10  2
2010  3  3  3  2  4  3  1  3  3  2  5  4
2011  1  3  0  3  1  2  0  1  1  2  5  4
2012  3  4  1  2  2  0  2  4  0  2  1  3
2013  4  3  4  4  0  5  1  1  3  0  1  0
2014  0  1  4  5  0  1  0  1  2  2  1  0
2015  1  2  6  0  2  1  3  4  2  2  0  2
2016  3  5  4  5  9  3  3  3  1  3  2  2
2017  5  1  2  2  5  3  1  1  1  1  4  4
2018  2  3  4  1  1  0  0  0  0  4  0  1
2019  2  3  1  4  2  3  1  3  1  2  2  1
2020  1  2  3  5 12  9  9  4  5  2  3  1
2021  1  0  2  0  1  1  0  2  0  1  0  0
2022  0  0  0  1  1  1  0  2  0  0  0  1
"""
BRD_CH11 = """
1980  -  -  -  1  0  0  0  0  1  1  0  0
1981  1  1  0  0  0  0  0  0  1  0  1  1
1982  0  1  0  3  1  1  2  2  0  1  1  1
1983  0  0  1  1  0  0  0  1  2  0  1  0
1984  0  0  0  1  0  0  1  0  1  2  0  1
1985  0  0  2  1  0  0  0  2  1  1  1  0
1986  2  1  2  0  0  0  1  1  1  1  1  1
1987  0  1  1  2  0  1  0  2  0  0  0  1
1988  1  4  1  0  1  0  3  2  0  0  0  0
1989  2  0  3  1  0  0  1  1  2  1  3  2
1990  2  2  4  5  2  2  1  0  2  4  3  3
1991  6  6  1  4  5  2  3  1  3  2  2  6
1992  8  2  2  1  2  3  0  4  2  7  1  0
1993  2  2  3  2  3  1  1  1  2  1  3  4
1994  2  2  1  0  0  0  0  1  1  0  3  1
1995  1  0  0  0  3  3  2  2  2  3  2  0
1996  3  0  0  1  1  1  2  2  3  2  0  1
1997  2  1  0  0  1  1  5  3  2  0  1  1
1998  5  3  0  0  1  2  3  1  4  4  4  4
1999  3  4  7  1  1  5  2  3  6  5  3  5
2000  7  6  4  8  9  9  4  7  7  5  6  8
2001  9  3  9 15  8  5  9  6  6  8  9 10
2002  9 12 10  7  8  7  2  3  3  7  7  6
2003  3  2  7  4  8  8  7  6  4  4  1  2
2004  1  4  7  1  2  3  0  1  3  3  2  2
2005  6  3  1  1  2  0  1  1  4  3  1  2
2006  1  1  3  0  1  1  1  1  2  2  0  1
2007  0  0  1  1  1  1  1  2  1  2  2  1
2008  3  3  2  3  1  1  3  2  3  3  4  7
2009  9  9 15  9  9  6  4  5  2  6  8  2
2010  3  3  2  2  1  2  1  3  2  2  5  2
2011  1  3  0  2  1  2  0  1  1  2  5  4
2012  3  4  1  2  2  0  2  4  0  2  1  3
2013  4  3  4  4  0  5  0  1  3  0  1  0
2014  0  1  4  5  0  1  0  1  2  2  1  0
2015  1  2  6  0  2  1  3  4  2  2  0  2
2016  3  5  3  5  9  3  3  3  0  3  2  2
2017  5  1  2  2  5  3  1  1  1  1  4  4
2018  2  3  4  1  1  0  0  0  0  4  0  1
2019  2  3  1  4  2  3  1  3  1  1  2  1
2020  1  2  3  5 12  9  9  4  5  2  3  1
2021  1  0  2  0  1  1  0  2  0  1  0  0
2022  0  0  0  1  1  1  0  2  0  0  0  1
"""
BRD_CH7 = """
1980  -  -  -  0  0  0  0  0  0  0  0  0
1981  0  0  0  0  0  0  0  0  0  0  0  0
1982  0  0  0  0  0  0  0  0  0  0  0  0
1983  0  0  0  0  0  0  0  0  0  0  0  0
1984  0  0  0  0  0  0  0  0  0  0  0  0
1985  0  0  0  0  0  0  0  0  0  0  0  0
1986  0  0  0  0  0  0  0  0  0  0  0  0
1987  0  0  0  0  0  0  0  0  0  0  0  0
1988  0  0  0  0  0  0  0  0  0  0  0  0
1989  0  0  0  0  0  0  0  0  0  0  0  0
1990  0  0  0  0  0  0  0  0  0  0  0  0
1991  0  0  0  0  0  0  0  0  0  0  0  0
1992  0  0  0  0  0  0  0  0  0  0  0  0
1993  0  0  0  0  0  0  0  0  0  0  0  0
1994  0  0  0  0  0  0  0  0  0  0  0  0
1995  0  0  0  0  0  0  0  0  0  0  0  0
1996  0  0  0  0  0  0  0  0  0  0  0  0
1997  0  0  0  0  0  0  0  0  0  0  0  0
1998  0  0  0  0  0  0  0  0  0  0  0  0
1999  0  0  0  0  0  0  0  0  0  0  0  0
2000  0  0  0  0  0  0  0  0  0  0  0  0
2001  0  0  0  0  0  0  0  0  0  0  0  0
2002  0  0  0  0  0  0  0  0  0  0  0  0
2003  0  0  0  0  0  1  0  0  0  0  0  0
2004  0  0  0  0  0  0  0  0  0  0  0  0
2005  0  0  0  0  0  0  0  0  0  0  0  0
2006  0  0  0  0  0  0  0  0  0  0  0  0
2007  0  0  0  0  0  0  0  0  0  0  0  0
2008  0  0  0  0  0  0  1  0  0  1  2  0
2009  1  0  1  0  0  0  1  1  1  0  2  0
2010  0  0  1  0  3  1  0  0  1  0  0  2
2011  0  0  0  1  0  0  0  0  0  0  0  0
2012  0  0  0  0  0  0  0  0  0  0  0  0
2013  0  0  0  0  0  0  1  0  0  0  0  0
2014  0  0  0  0  0  0  0  0  0  0  0  0
2015  0  0  0  0  0  0  0  0  0  0  0  0
2016  0  0  1  0  0  0  0  0  1  0  0  0
2017  0  0  0  0  0  0  0  0  0  0  0  0
2018  0  0  0  0  0  0  0  0  0  0  0  0
2019  0  0  0  0  0  0  0  0  0  1  0  0
2020  0  0  0  0  0  0  0  0  0  0  0  0
2021  0  0  0  0  0  0  0  0  0  0  0  0
2022  0  0  0  0  0  0  0  0  0  0  0  0
"""

if __name__ == "__main__":
    main()
