#!/usr/bin/env python3
"""Falcom 3723 v9 — issuer-calibrated recognition timing + decomposed "other" pool.

All amounts ¥億. Research model, NOT issuer guidance and NOT an audited bridge.

What changes vs v8 (and why)
----------------------------
1. Japan year-1 recognition.  v8 spread Japan year-1 paid units over 12 months
   (25% in month 1, uniform within the month), so Sky1 had only 9,000 Japan
   copies (¥0.486億) before 2025-09-30.  Falcom's own quarterly segment data
   show FY25 Q4 Product jumped ¥5.0億 above the Q3 run-rate in the Sky1 launch
   quarter, and FY26 Q3 Product jumped again before the 2026-07-16 Kyoto launch.
   Product is recognised on shipment (FY25 filing), so Japanese launch sell-in
   lands in the launch fiscal quarter.  v9 books each title's Japan YEAR-1
   economics on its release date (parameter JP_YEAR1_AT_LAUNCH).  Lifetime
   units, yen/unit and the 1m / 750k / annual major+medium inputs are unchanged.
2. Category.  Year-1 Japan launch economics -> Product (physical sell-in via the
   domestic distributor).  Later-year Japan economics -> Licensing (download
   distribution is a Licensing line in the FY25 filing; SIE is a disclosed
   licensing customer).  Overseas -> Licensing with the same recoupable-MG rule.
3. "Other" pool.  v8 carried one undifferentiated ¥12億.  v9 decomposes it into
   catalogue licensing + localisation/port/new-licence flow + other Product,
   each with an FY25 issuer-derived anchor.  Base total 13.5 (investor
   decision 2026-10-20; v8 used 12).  12 and 13.5-decaying are shown beside it.
4. Valuation (2026-10-20 red-team fix).  Value TODAY = (multiple x mean FY27-29
   EBIT + estimated net cash at 2026-09-30) / shares.  The earlier 2028
   cash-bridge + PV method double counted FY27-28 earnings and added an
   already-ex-dividend ¥60; both removed.

Nothing here identifies per-title contracts.  Every per-unit, MG and pool
number is an investor assumption unless tagged ISSUER.
"""
import sys
from dataclasses import dataclass, field
from datetime import date
from pathlib import Path
from statistics import mean, pstdev

sys.path.insert(0, str(Path(__file__).resolve().parent))
from archive.cohort_model_v5_rolling_catalogue import FY_COSTS
from archive.cohort_model_v6_gaap_recognition import recognise_foreign, EPS_SHARES_JUN26
from archive.cohort_model_v7_sky2_750k import (
    amended_titles, title_components, model as v8_model, valuation as v8_valuation,
    CASH_2YR_HYPOTHESIS, EV_EBIT_HYPOTHESIS,
)
import cohort_model_v8_fy2026_reconciliation as v8

# ---------------------------------------------------------------- ISSUER data
# Quarter totals and segment amounts from Falcom 決算短信 / 有価証券報告書.
# FY25 H1/9M segments are BACK-SOLVED from YoY % printed in the FY26 filings
# (FY25 9M sum reproduces the reported 10.40596 to within ¥0.005億).
ISSUER_QUARTERS = {
    # key: (start, end_exclusive, product, licensing, status)
    'FY25H1': (date(2024, 10, 1), date(2025, 4, 1), 1.16/0.945, 14.19/2.948, 'derived'),
    'FY25Q3': (date(2025, 4, 1), date(2025, 7, 1), 2.94/1.668-1.16/0.945,
               19.09/2.21-14.19/2.948, 'derived'),
    'FY25Q4': (date(2025, 7, 1), date(2025, 10, 1), 7.31131-2.94/1.668,
               18.81851-19.09/2.21, 'derived'),
    'FY26Q1': (date(2025, 10, 1), date(2026, 1, 1), 0.49, 8.88, 'issuer'),
    'FY26Q2': (date(2026, 1, 1), date(2026, 4, 1), 1.16-0.49, 14.19-8.88, 'issuer'),
    'FY26Q3': (date(2026, 4, 1), date(2026, 7, 1), 2.94-1.16, 19.09-14.19, 'issuer'),
}
FY26Q4_GUIDED_TOTAL = 43.0 - 22.03704          # post-period revision, 2026-10-07
FY25_PRODUCT, FY25_LICENSING = 7.31131, 18.81851
FY25_COGS = 3.10737                            # ISSUER: 42.5% of FY25 Product

# FY25 major customers (ISSUER, 有価証券報告書 2025, ¥億).  Interpretation is
# INFERENCE: Konami ≈ 79-80% of Product in both FY24 and FY25 -> most likely
# the domestic packaged-goods distribution channel (Product), not a licence.
# 2026-10-20 research: KDE/Product 79-88% in FY2020/23/24/25 vs KDE/Licensing
# 27-60%; KDE largest receivable at FY24 year-end right after Kai launch
# (2024-09-26, Product booked on shipment); Licensing recognition policy is
# "主に海外"; business-model chart routes Product via 小売店/卸売業者.
# Domestic DIGITAL is Licensing, paid mainly by SIE. Confidence ~85%.
CUSTOMERS = {
    'Konami Digital Entertainment': {'FY24': 5.16033, 'FY25': 5.75760,
                                     'role': 'inferred JP packaged distributor (Product)'},
    'NIS America':                  {'FY24': 7.52117, 'FY25': 3.49409,
                                     'role': 'English localisation of earlier titles (Licensing)'},
    'Clouded Leopard Ent.':         {'FY24': 2.93189, 'FY25': 3.31294,
                                     'role': 'Asian-language editions (Licensing)'},
    'GungHo Online Ent. America':   {'FY24': None,    'FY25': 2.91692,
                                     'role': 'Sky1 worldwide publisher (Licensing)'},
    'Sony Interactive Ent.':        {'FY24': 3.36196, 'FY25': 2.44330,
                                     'role': 'PS Store download licence (Licensing)'},
}

# ------------------------------------------------------------ v9 parameters
JP_YEAR1_AT_LAUNCH = 1.0   # share of year-1 Japan economics booked on release date
MG_SHARE = 0.25            # unchanged investor input; see mg_customer_bound()

@dataclass(frozen=True)
class OtherPool:
    """Non-named revenue, ¥億/yr.  Base 13.5 APPROVED by investor 2026-10-20
    (v8 used 12.0).  Evidence: FY25 non-Sky1 14.42; FY26 9M non-named
    annualised ~16.5, of which ~25-35% is one-off localisation MG."""
    catalogue_licensing: float = 9.0   # FY25 other licensing ex-NISA ≈ 8.5; FY26 run-rate higher
    localisation_and_ports: float = 2.5  # NISA/XSEED/Edia/NEOWIZ-type flow; FY25 NISA was 3.49
    other_product: float = 2.0         # music, goods, older packaged titles; FY25 ≈ 2.3-2.45

    @property
    def total(self):
        return self.catalogue_licensing + self.localisation_and_ports + self.other_product

OTHER_CASES = {
    'base':      OtherPool(),
    'v8_12':     OtherPool(catalogue_licensing=8.5, localisation_and_ports=1.5, other_product=2.0),
    'low':      OtherPool(catalogue_licensing=7.5, localisation_and_ports=0.0, other_product=1.5),
    'decay15':   None,  # handled in run(): base decays 15%/yr from FY26
    'high':      OtherPool(catalogue_licensing=9.0, localisation_and_ports=3.5, other_product=2.5),
    # 2026-10-20 research recommendation, PENDING investor approval as Base:
    # FY26 non-named run-rate ~16.5 x 65-75% recurring share ~= 10.7-12.4, plus
    # new ports/remasters (Edia 7 SKUs, NEOWIZ, NISA Zero/Azure) ~2-3/yr.
    # Not 15: external 'flagship = 12 months' definition counts named-title
    # tails as 'other'; in this model those tails are already in the cohorts.
    'recommended': OtherPool(catalogue_licensing=9.0, localisation_and_ports=2.5, other_product=2.0),
}

# ------------------------------------------------------- valuation (today)
# 2026-10-20 red-team fix (P0-1): the previous 2028-09-30 cash bridge rolled
# FY27-28 net income into cash AND capitalised mean FY27-29 EBIT at 10x, so
# FY27-28 earnings were counted twice.  Base now values the company TODAY:
#   value/share = (multiple x mean FY27-29 EBIT + net cash today) / shares
# FY27-29 earnings enter once, through the multiple.  No discounting step and
# no dividends added (P0-2: the FY26 ¥60 had a 2026-09-30 record date, so a
# buyer on 2026-10-20 does not receive it; it is a cash outflow of the company).
CASH_JUN26 = 107.99771              # ISSUER: cash & deposits 2026-06-30
NI_9M_FY26 = 8.89404                # ISSUER: 9M net income
NI_FY26_GUIDE = 19.0                # ISSUER: revised forecast 2026-10-07 (replace with actual ~2026-11-11)
DIV_FY26_YEN = 60                   # ISSUER: ¥30 ordinary + ¥30 commemorative, record date 2026-09-30, paid ~Dec 2026
WORKING_CAPITAL_ADJ = 4.0           # assumption: Sky2 launch receivables at 2026-09-30 + ~2.9 contract liabilities already in cash
OPERATING_CASH_BUFFER = 30.0        # assumption: ~1.8x annual opex kept for development
EV_EBIT_MULTIPLE = EV_EBIT_HYPOTHESIS  # 10x and 8x shown side by side (investor decision 2026-10-20)

# ------------------------------------------------------------- cost path
# Investor decision 2026-10-20: add +5 hires per year (cumulative from FY26
# headcount) ON TOP of the earlier 15.5/16.5/17.0 path.  Full-loaded cost per
# FTE ¥750萬 (FY25 average salary ¥557萬 + social insurance/bonus/overhead);
# full-year cost assumed for each year's hires (conservative).
HIRES_PER_YEAR = 5
COST_PER_FTE = 0.075                 # ¥億 per FTE-year (assumption)
def hiring_cost(fy):
    return HIRES_PER_YEAR * (fy - 2026) * COST_PER_FTE
BASE_COSTS = {fy: FY_COSTS[fy] + hiring_cost(fy) for fy in FY_COSTS}   # 15.875 / 17.25 / 18.125
TODAY = date(2026, 10, 20)

def net_cash_today(shares=EPS_SHARES_JUN26):
    """Estimated excess cash at 2026-09-30 (FY26 year end), after the ¥60
    dividend liability, working-capital timing and an operating buffer."""
    rows = [
        ('Cash 2026-06-30 (issuer)', CASH_JUN26),
        ('+ FY26 Q4 net income (guide 19.0 - 9M 8.89)', NI_FY26_GUIDE - NI_9M_FY26),
        ('- FY26 dividend ¥60 (record 9/30, paid Dec)', -DIV_FY26_YEN * shares / 1e8),
        ('- working-capital timing (launch receivables)', -WORKING_CAPITAL_ADJ),
        ('- operating cash buffer', -OPERATING_CASH_BUFFER),
    ]
    return sum(v for _, v in rows), rows

def value_today(mean_ebit, multiple=EV_EBIT_MULTIPLE, shares=EPS_SHARES_JUN26):
    cash, _ = net_cash_today(shares)
    return (mean_ebit * multiple + cash) * 1e8 / shares

def fy_of(d):
    return d.year + 1 if d.month >= 10 else d.year

# ------------------------------------------------------- title recognition
def title_jp_v9(t, phi=JP_YEAR1_AT_LAUNCH, jp_scale=1.0):
    """Japan economics by FY, split into launch Product and later Licensing.

    With jp_scale=1 lifetime Japan economics are IDENTICAL to v8; only
    timing/category move.  jp_scale scales Japan YEAR-1 units only (sensitivity).
    """
    from dataclasses import replace
    j_all, _ = title_components(t)
    j_y1, _ = title_components(replace(t, paid_units_by_age_year=(t.paid_units_by_age_year[0],)))
    y1_total = sum(j_y1.values())
    product, licence = {}, {}
    rel = fy_of(t.release)
    product[rel] = phi * y1_total * jp_scale
    for y in set(j_all) | set(j_y1):
        later = j_all.get(y, 0) - j_y1.get(y, 0)
        licence[y] = later + (1 - phi) * j_y1.get(y, 0) * jp_scale
    return product, licence

def title_foreign_v9(t, mg_share=MG_SHARE):
    _, o = title_components(t)
    return recognise_foreign(o, mg_share * sum(o.values()), fy_of(t.release))

def named_by_fy(phi=JP_YEAR1_AT_LAUNCH, mg_share=MG_SHARE, jp_scale=1.0, titles=None):
    out = {}
    for t in titles or amended_titles():
        p, l = title_jp_v9(t, phi, jp_scale)
        f = title_foreign_v9(t, mg_share)
        for y in range(2025, 2038):
            row = out.setdefault(y, {'product': 0.0, 'licensing': 0.0})
            row['product'] += p.get(y, 0)
            row['licensing'] += l.get(y, 0) + f.get(y, 0)
    return out

# --------------------------------------------------------- quarterly backtest
def named_in_window(start, stop, phi=JP_YEAR1_AT_LAUNCH, mg_share=MG_SHARE):
    """Named-title revenue booked in [start, stop): Product (launch) and Licensing."""
    product = licence = 0.0
    for t in amended_titles():
        if t.release >= stop:
            continue
        L = sum(title_components(t)[1].values())
        mg = mg_share * L
        before = v8.geo_economic_in_window(t, t.release, start, True) if start > t.release else 0.0
        through = v8.geo_economic_in_window(t, t.release, stop, True)
        licence += max(0, through - mg) - max(0, before - mg) + (mg if start <= t.release < stop else 0)
        # Japan: year-1 at release date, later years as v8 curve
        y1_end = date(t.release.year + 1, t.release.month, t.release.day)
        if start <= t.release < stop:
            product += phi * t.paid_units_by_age_year[0] * t.jp_fraction * t.jp_net[0] / 1e8
        jp_window = v8.geo_economic_in_window(t, max(start, t.release), stop)
        jp_y1_part = v8.geo_economic_in_window(t, max(start, t.release), min(stop, y1_end)) if start < y1_end else 0
        licence += jp_window - phi * jp_y1_part
    return product, licence

def quarterly_backtest(phi=JP_YEAR1_AT_LAUNCH, mg_share=MG_SHARE):
    """Implied non-named revenue per quarter = issuer total - modelled named titles.

    A correct timing model should leave a roughly STABLE implied 'other' run-rate.
    """
    rows = []
    for k, (s, e, p, l, status) in ISSUER_QUARTERS.items():
        if e <= date(2025, 7, 1):     # before Sky1: no named titles, pure 'other'
            np_, nl = 0.0, 0.0
        else:
            np_, nl = named_in_window(s, e, phi, mg_share)
        months = (e.year - s.year) * 12 + e.month - s.month
        rows.append(dict(period=k, status=status, issuer_product=p, issuer_licensing=l,
                         named_product=np_, named_licensing=nl,
                         implied_other_product=p - np_, implied_other_licensing=l - nl,
                         implied_other_per_quarter=(p + l - np_ - nl) * 3 / months))
    s, e = date(2026, 7, 1), date(2026, 10, 1)
    np_, nl = named_in_window(s, e, phi, mg_share)
    rows.append(dict(period='FY26Q4g', status='guided', issuer_product=None, issuer_licensing=None,
                     named_product=np_, named_licensing=nl, implied_other_product=None,
                     implied_other_licensing=None,
                     implied_other_per_quarter=FY26Q4_GUIDED_TOTAL - np_ - nl))
    return rows

# ---------------------------------------------------------------- bridges
def fy25_other_bridge(mg_share=MG_SHARE):
    """FY25 issuer totals less modelled Sky1 under v9 timing."""
    sky1 = next(t for t in amended_titles() if t.name == 'Sky_1st')
    p, l = title_jp_v9(sky1)
    f = title_foreign_v9(sky1, mg_share)
    sky1_product, sky1_licence = p.get(2025, 0), l.get(2025, 0) + f.get(2025, 0)
    other_lic = FY25_LICENSING - sky1_licence
    return {
        'sky1_FY25_product': sky1_product,
        'sky1_FY25_licensing': sky1_licence,
        'other_product': FY25_PRODUCT - sky1_product,
        'other_licensing': other_lic,
        'of_which_NISA_localisation': CUSTOMERS['NIS America']['FY25'],
        'other_licensing_ex_NISA': other_lic - CUSTOMERS['NIS America']['FY25'],
        'other_total': FY25_PRODUCT + FY25_LICENSING - sky1_product - sky1_licence,
    }

def mg_customer_bound():
    """Upper bound on Sky1 FY25 foreign MG share from the customer table.

    Sky1 overseas rights sit with GungHo America (West) and CLE (Asia).  Their
    combined FY25 revenue caps what Sky1 overseas could have booked in FY25;
    CLE also sells other Falcom titles, so the true share is lower still.
    """
    sky1 = next(t for t in amended_titles() if t.name == 'Sky_1st')
    L = sum(title_components(sky1)[1].values())
    cap = CUSTOMERS['GungHo Online Ent. America']['FY25'] + CUSTOMERS['Clouded Leopard Ent.']['FY25']
    return {'sky1_foreign_lifetime': L, 'customer_cap': cap, 'max_mg_share': cap / L}

# ------------------------------------------------------------------- run
def run(phi=JP_YEAR1_AT_LAUNCH, mg_share=MG_SHARE, pool=OtherPool(), decay=0.0,
        cost=None, jp_scale=1.0, multiple=EV_EBIT_MULTIPLE):
    cost = cost or BASE_COSTS
    named = named_by_fy(phi, mg_share, jp_scale)
    res = {}
    for y in range(2026, 2030):
        other = pool.total * ((1 - decay) ** (y - 2026))
        rev = named[y]['product'] + named[y]['licensing'] + other
        row = {'named_product': named[y]['product'], 'named_licensing': named[y]['licensing'],
               'other': other, 'revenue': rev}
        if y in cost:
            row['cost'] = cost[y]
            row['ebit'] = rev - cost[y]
        res[y] = row
    m = mean(res[y]['ebit'] for y in (2027, 2028, 2029))
    res['mean_ebit'] = m
    res['net_cash_today'] = net_cash_today()[0]
    res['value_today'] = value_today(m, multiple)
    res['value_today_by_multiple'] = {k: value_today(m, k) for k in (8, 10, 12)}
    # v8-style reference only: future-dated flat ¥90億 cash, undiscounted
    res['v8_style_future_value'] = (m * EV_EBIT_HYPOTHESIS + CASH_2YR_HYPOTHESIS) * 1e8 / EPS_SHARES_JUN26
    res['fy26_gap_vs_43'] = 43.0 - res[2026]['revenue']
    return res

def scenario_table():
    v8b, _ = v8_model(mg_share=.25, unassigned=12)
    v8m = v8_valuation(v8b)['mean_ebit']
    out = [('v8 Case A (reference, no hiring)', [v8b[y]['conditional_ebit'] for y in (2027, 2028, 2029)],
            v8m, value_today(v8m), value_today(v8m, 8))]
    hi_cost = {y: BASE_COSTS[y] + d for y, d in ((2027, .5), (2028, 1.0), (2029, 2.0))}
    no_hire = dict(FY_COSTS)
    cases = {
        'BASE: other 13.5 + 5 hires/yr': run(),
        'Other 12 (red-team preference)': run(pool=OTHER_CASES['v8_12']),
        'Other 13.5 decaying 5%/yr (risk version)': run(decay=.05),
        'No extra hiring (old 15.5/16.5/17.0)': run(cost=no_hire),
        'MG 20% (customer-table consistent)': run(mg_share=.20),
        'Other low (=9.0)': run(pool=OTHER_CASES['low']),
        'Other high (=15.0)': run(pool=OTHER_CASES['high']),
        'Cost +0.5/+1.0/+2.0 on top of Base': run(cost=hi_cost),
        'JP year-1 units +33% (all titles)': run(jp_scale=1.33),
        'BEAR: other low + cost +0.5/1/2': run(pool=OTHER_CASES['low'], cost=hi_cost),
        'BULL: other high + JP year-1 +33%': run(pool=OTHER_CASES['high'], jp_scale=1.33),
    }
    for k, r in cases.items():
        out.append((k, [r[y]['ebit'] for y in (2027, 2028, 2029)], r['mean_ebit'],
                    r['value_today'], r['value_today_by_multiple'][8]))
    return out

if __name__ == '__main__':
    print('== FY25 "other" bridge under v9 timing (¥億) ==')
    for k, v in fy25_other_bridge().items():
        print(f'  {k:32s} {v:7.2f}')
    b = mg_customer_bound()
    print(f"\nMG bound: Sky1 foreign lifetime {b['sky1_foreign_lifetime']:.2f}; "
          f"GungHo America + CLE FY25 {b['customer_cap']:.2f} -> max MG share {b['max_mg_share']:.1%}")
    print('\n== Quarterly backtest: implied non-named revenue per quarter ==')
    for r in quarterly_backtest():
        print(f"  {r['period']:8s} {r['status']:8s} named P {r['named_product']:5.2f} "
              f"L {r['named_licensing']:5.2f} -> implied other {r['implied_other_per_quarter']:5.2f}")
    print('  (same with v8 timing, phi=0):',
          [round(r['implied_other_per_quarter'], 2) for r in quarterly_backtest(phi=0.0)])
    base = run()
    print(f"\nFY26 v9: named {base[2026]['named_product']+base[2026]['named_licensing']:.2f} + other "
          f"{base[2026]['other']:.2f} = {base[2026]['revenue']:.2f}; gap to 43 = {base['fy26_gap_vs_43']:.2f}")
    print('\n== Net cash today (est. 2026-09-30) ==')
    tot, rows = net_cash_today()
    for k, v in rows:
        print(f'  {k:48s} {v:7.2f}')
    print(f'  {"= net cash used in valuation":48s} {tot:7.2f}')
    print('\n== Base value today (%s) ==' % TODAY)
    for k, v in base['value_today_by_multiple'].items():
        print(f'  {k:>2d}x mean FY27-29 EBIT {base["mean_ebit"]:.2f} + net cash {tot:.1f}  ->  ¥{v:,.0f}/share')
    print('\n== Scenarios: FY27/28/29 EBIT | mean | today ¥ @10x | today ¥ @8x ==')
    for name, e, m, v10, v8x in scenario_table():
        print(f'  {name:40s} {e[0]:6.2f} {e[1]:6.2f} {e[2]:6.2f} | {m:6.2f} | ¥{v10:5.0f} | ¥{v8x:5.0f}')
    print('\nValue today = (multiple x mean FY27-29 EBIT + est. net cash 2026-09-30) / 9,867,090 shares. '
          'Each ¥1億 mean EBIT ≈ ¥101 at 10x. Scenarios, not a target price.')
