"""Канон доходных строк Марса — единственный источник правды.

Раньше справочник был размазан по трём местам и расходился: финмодель (12 строк),
плановые операции Финтабло (6), направления AlfaCRM (7). Одно и то же понятие
называлось по-разному, а «Съёмки» в CRM и «Мероприятия внешние» в финмодели
вообще про разное (см. docs/income-canon-design-2026-07-31.md).

Канон — направления CRM: только у них есть измеримый факт. Мерч и внешние мероприятия
собираются в строку «Прочее». Онлайн-подписка стоит отдельной строкой с нулевым фактом:
клуб не запущен, направления в CRM нет, но строка заведена заранее — чтобы при запуске
хватило прописать направление, не перестраивая канон, планки и два листа.
"""
import collections

# CAT_* — статьи Финтабло, куда падают плановые операции
CAT_B2C = 1543079
CAT_B2B = 1543085

# slug         — ключ в externalId `plansync:income:MM.YYYY:{slug}`
# name         — человеческое имя (лист, отчёт, чат)
# crm_dirs     — направления AlfaCRM, дающие факт строки (пусто = факт из «прочего»)
# model_rows   — строки финмодели (индексы листа, как в cashflow.REVENUE_SECTIONS)
# category     — статья Финтабло для плановой операции
# risky        — вероятностная строка: может не случиться, runway считаем и без неё
Line = collections.namedtuple("Line", "slug name crm_dirs model_rows category risky")

LINES = [
    Line("uroki",   "1-1",                    ["1-1"],                    [4],          CAT_B2C, False),
    Line("gruppy",  "Групповые",              ["Групповые"],              [6],          CAT_B2C, False),
    Line("hor",     "Хор",                    ["Хор"],                    [5],          CAT_B2C, False),
    Line("arenda",  "Аренда",                 ["Аренда"],                 [3],          CAT_B2C, False),
    Line("iventy",  "Внутренние мероприятия", ["Внутренние мероприятия"], [9],          CAT_B2C, True),
    Line("cover",   "Cover session",          ["Съёмки"],                 [10],         CAT_B2C, True),
    Line("b2b",     "B2B",                    ["B2B"],                    [14, 15, 16], CAT_B2B, True),
    # клуб не запущен: направления в CRM нет, факт всегда 0. Строка заведена заранее,
    # чтобы при запуске хватило прописать направление в crm_dirs (Наташа, 31.07.2026)
    Line("podpiska", "Онлайн-подписка",       [],                         [7],          CAT_B2C, True),
    Line("prochee", "Прочее",                 [],                         [8, 11],      CAT_B2C, True),
]

# Строка, куда падает доход CRM вне направлений (мерч, внешние мероприятия).
OTHER_SLUG = "prochee"

_BY_SLUG = {l.slug: l for l in LINES}


def by_slug(slug):
    """Строка канона по slug. KeyError, если slug неизвестен — молчать нельзя,
    иначе прогноз уедет в никуда."""
    return _BY_SLUG[slug]


def crm_fact_for(line, dirs, other=0.0):
    """Факт одной строки канона. Чистая функция — её и тестируем без сети.

    Строки без направлений дают 0 (онлайн-подписка, пока клуб не запущен), кроме
    OTHER_SLUG: туда сваливается доход CRM вне направлений.
    """
    val = sum(dirs.get(d, 0.0) for d in line.crm_dirs)
    return val + other if line.slug == OTHER_SLUG else val


def crm_fact(mm, token=None, pay_items=None):
    """{slug: сумма} — факт месяца mm (MM.YYYY) из AlfaCRM по канону.

    «Прочее» — весь доход CRM, не попавший ни в одно направление (мерч, внешние
    мероприятия): margins сознательно держит его отдельно, см. INCOME_EXCLUDE_NAMES.

    pay_items — справочник статей оплаты AlfaCRM; передавай его, если считаешь факт
    за несколько месяцев подряд (напр. plan_fact.py по всем месяцам года). Без него
    margins.income_with_other лезет за этим справочником в AlfaCRM на каждый вызов —
    лишний запрос на каждый месяц там, где список один и тот же весь прогон.
    """
    import margins
    dirs, other = margins.income_with_other(mm, token, pay_items)
    return {l.slug: crm_fact_for(l, dirs, other) for l in LINES}


def crm_fact_from_month_data(income_m):
    """{slug: сумма} — то же самое, что crm_fact(), но из уже посчитанных данных
    margins.income_all_months() (один проход по AlfaCRM на весь год), а не отдельного
    HTTP-похода на каждый месяц.

    income_m — запись ОДНОГО месяца из результата income_all_months (плоский словарь
    {направление: сумма} + служебные ключи "__crm_other__"/"__income_detail__", см.
    margins._collect_income). Используй при прогоне по многим месяцам подряд
    (plan_fact.py по всем 12 месяцам года) — раньше там на каждый месяц заново гоняли
    полную пагинацию /v2api/{br}/pay/index по всем филиалам (см. находку #4 финального
    ревью), хотя income_all_months уже даёт то же самое одним проходом.
    """
    import margins
    dirs = {d: income_m.get(d, 0.0) for d in margins.DIRECTIONS}
    other = sum(income_m.get("__crm_other__", {}).values())
    return {l.slug: crm_fact_for(l, dirs, other) for l in LINES}


def plan_from_fintablo(mm, all_income=None):
    """{slug: сумма} — прогноз месяца mm из плановых операций Финтабло.

    Пусто ({}), если планок на месяц нет: это норма (дальние месяцы считаются
    по финмодели), а не сбой.
    """
    import core
    if all_income is None:
        all_income = core.ft_transactions(group="income")
    prefix = "plansync:income:%s:" % mm
    res = {}
    for t in all_income:
        ext = t.get("externalId") or ""
        if t.get("isPlan") and ext.startswith(prefix):
            slug = ext[len(prefix):]
            res[slug] = res.get(slug, 0.0) + float(t.get("value") or 0)
    return res


def plan_from_model(plan, colidx, ym):
    """{slug: сумма} — план месяца ym=(год, месяц) из финмодели.

    plan/colidx — то, что отдают cashflow.read_plan() и cashflow.month_columns().
    """
    import cashflow
    return {l.slug: cashflow.pv(plan, colidx, l.model_rows, ym) for l in LINES}
