#!/usr/bin/env python3
"""
Занос наличных расходов из Google-таблицы Оли в Финтабло (счёт «Наличные»).

Использование:
    python enter_cash_expenses.py             # dry-run: показать план
    python enter_cash_expenses.py --commit     # реально занести

Читает таблицу Оли (CSV-экспорт), берёт строки Способ оплаты = «Саша» И Статус = «Оплачено»,
заносит в Финтабло на счёт «Наличные» как расход по статье (маппинг ниже), дата = «Дата платежа».
«Точка» не трогает (падает из банка). «Не оплачено» не трогает (это прогноз).
Дедуп: externalId = olya:<хеш(дата|сумма|назначение)>; повтор пропускается.

⚠️ Если Оля ПРАВИТ уже оплаченную строку (меняет сумму или дату), хеш меняется и скрипт
занёс бы её второй раз. Правку суммы ловит страховка по (дата, сумма) не всегда, поэтому
отдельно печатаем «сироты»: заносы olya:*, которых больше нет в таблице (см. orphans).
"""
import hashlib, json, os, re, sys
from collections import defaultdict

import core
import olya_sheet

NAL = 154399  # счёт «Наличные»
norm = core.norm
parse_sum = olya_sheet.parse_sum        # разбор таблицы Оли — общий модуль


def ext_id(date, amount, naznach):
    h = hashlib.md5(f"{date}|{amount}|{naznach}".encode()).hexdigest()[:12]
    return f"olya:{h}"


def paid_rows(rows):
    """Строки таблицы «Саша + Оплачено» → (список записей, список неразобранных). Чистая функция."""
    fact, bad = [], []
    for r in rows:
        if norm(r.get('Способ оплаты')) != 'саша' or norm(r.get('Статус')) != 'оплачено':
            continue
        amt = parse_sum(r.get('Сумма'))
        date = (r.get('Дата платежа') or '').strip()
        naznach = (r.get('Назначение') or '').strip()
        rec = {'date': date, 'amount': amt, 'art': r.get('Статья расхода'),
               'ftname': olya_sheet.row_category(r),
               'naznach': naznach,
               'kontr': (r.get('Контрагент') or '').strip()}
        if not (rec['ftname'] and amt and re.match(r'\d{2}\.\d{2}\.\d{4}$', date)):
            bad.append(rec); continue
        rec['ext'] = ext_id(date, amt, naznach)
        fact.append(rec)
    return fact, bad


def twin_orphan(row, orphans):
    """Сирота = ТА ЖЕ строка таблицы, у которой поменяли дату после оплаты.

    Узнаём по сумме + назначению: externalId считается от даты, поэтому правка даты рвёт
    связь и раньше строка заносилась ВТОРОЙ раз (или её случайно глушила страховка
    «дата+сумма» — так вышло с двумя разными репбазами по 7 100 ₽).
    externalId менять нельзя (в PUT такого поля нет), поэтому просто правим дату.
    Чистая функция.
    """
    key = norm(row['naznach'])[:60]
    if not key:
        return None
    for t in orphans:
        if abs(float(t['value']) - row['amount']) < 0.01 and norm(t.get('description') or '').startswith(key):
            return t
    return None


def plan_actions(fact, tx):
    """Что делать с каждой оплаченной строкой таблицы → (to_add, to_fix, skip_ext, skip_da, orphans).

    to_fix — [(операция Финтабло, строка)]: та же трата, но дата в таблице изменилась.
    Порядок важен: сначала ищем «свою» операцию (ext → сирота-двойник), и только потом
    включаем грубую страховку по (дата, сумма) — иначе чужой ручной платёж той же суммы
    заблокировал бы легитимный занос. Чистая функция.
    """
    seen_ext = {t.get('externalId') for t in tx if t.get('externalId')}
    seen_da = defaultdict(int)
    for t in tx:
        if t.get('group') == 'outcome':
            seen_da[(t['date'], round(t['value'], 2))] += 1
    live = {f['ext'] for f in fact}
    orphans = [t for t in tx if (t.get('externalId') or '').startswith('olya:')
               and t.get('externalId') not in live and t.get('group') == 'outcome']

    free = list(orphans)
    to_add, to_fix, skip_ext, skip_da = [], [], 0, []
    for f in fact:
        if f['ext'] in seen_ext:
            skip_ext += 1; continue
        twin = twin_orphan(f, free)
        if twin:
            free.remove(twin)                       # одна сирота — одна строка
            if twin['date'] != f['date']:
                to_fix.append((twin, f))
            else:
                skip_ext += 1                       # уже занесено, ext просто от старой даты
            continue
        if seen_da.get((f['date'], f['amount']), 0) > 0:
            seen_da[(f['date'], f['amount'])] -= 1; skip_da.append(f); continue
        to_add.append(f)
    return to_add, to_fix, skip_ext, skip_da, free


def main():
    commit = '--commit' in sys.argv
    rows = olya_sheet.fetch_rows()
    print(f"строк в таблице: {len(rows)}  |  режим: {'ЗАПИСЬ' if commit else 'dry-run'}")

    cats = olya_sheet.outcome_cats_by_name()
    fact, badcat = paid_rows(rows)
    for f in fact:
        f['catid'] = cats.get(f['ftname'])
    badcat += [f for f in fact if not f['catid']]
    fact = [f for f in fact if f['catid']]

    # дедуп против Финтабло «Наличные» расходов (текущий год + прошлый — платежи на стыке)
    tx = (core.ft_transactions(moneybagId=NAL)
          + core.ft_transactions(year=core.FIN_YEAR - 1, moneybagId=NAL))
    to_add, to_fix, skip_ext, skip_da, orphans = plan_actions(fact, tx)

    print(f"\nфакт-наличка (Саша+Оплачено): {len(fact)} | к заносу: {len(to_add)} | "
          f"поправить дату: {len(to_fix)} | пропущено по externalId (точный дубль): {skip_ext}")
    for t, f in to_fix:
        print(f"   ✎ {t['date']} → {f['date']} | {f['amount']:>9,.0f} | {f['naznach'][:45]} "
              f"(дату поменяли в таблице — правлю, а не заношу второй раз)")
    for f in to_add:
        print(f"   {f['date']} | {f['amount']:>9,.0f} | {f['art']:<24} | {f['naznach'][:40]}")
    # Пропуски по (дата, сумма) — страховка от ручных дублей; может съесть легитимный
    # второй расход в тот же день на ту же сумму. Печатаем построчно для проверки глазами.
    if skip_da:
        print(f"⚠ пропущено как возможный дубль по (дата, сумма): {len(skip_da)} — проверь, нет ли легитимных:")
        for f in sorted(skip_da, key=lambda x: x['date']):
            print(f"   ⏭ {f['date']} | {f['amount']:>9,.0f} | {f['art']:<24} | {f['naznach'][:40]}")
    if orphans:
        print(f"\n⚠ в Финтабло есть {len(orphans)} заносов из таблицы Оли, которых в таблице уже нет "
              f"(строку удалили или изменили сумму/назначение) — проверь, не дубль ли:")
        for t in sorted(orphans, key=lambda t: t['date'])[:15]:
            print(f"   ? {t['date']} | {float(t['value']):>9,.0f} | {(t.get('description') or '')[:45]}")
    if badcat:
        print("⚠ не разобрал:", [(b['art'], b['date'], b['amount']) for b in badcat[:5]])
    bal = core.ft_moneybag_balance(NAL)
    print(f"\nостаток «Наличные» {bal:,.0f} → после ≈ {bal - sum(f['amount'] for f in to_add):,.0f}")

    if not commit:
        print("\n(dry-run — ничего не записано. --commit чтобы занести.)"); return
    for t, f in to_fix:                  # дата в таблице изменилась → правим существующую операцию
        core.ftmut('PUT', f"/v1/transaction/{t['id']}",
                   {'date': f['date'], 'factMonth': f['date'][3:]})
        print(f"  ✎ дата {t['date']} → {f['date']} ({f['amount']:,.0f} ₽)")
    log = []
    for f in to_add:
        desc = f['naznach'] + (f" — {f['kontr']}" if f['kontr'] else '')
        body = {'value': f['amount'], 'group': 'outcome', 'moneybagId': NAL, 'date': f['date'],
                'categoryId': f['catid'], 'factMonth': f['date'][3:], 'externalId': f['ext'],
                'description': desc[:250]}
        res = core.ftmut('POST', '/v1/transaction', body)
        tid = res.get('id') if isinstance(res, dict) else None
        if tid:
            log.append({'id': tid, 'date': f['date'], 'value': f['amount'], 'art': f['art']})
        else:
            print("  ✗", f, res)
    os.makedirs(os.path.join(core.HERE, 'exports'), exist_ok=True)
    out = os.path.join(core.HERE, 'exports', 'olya_cash_expenses.json')
    old = json.load(open(out)) if os.path.exists(out) else []
    with open(out, 'w', encoding='utf-8') as fh:
        json.dump(old + log, fh, ensure_ascii=False, indent=1)
    print(f"\nзанесено: {len(log)} на {sum(x['value'] for x in log):,.0f} | "
          f"остаток «Наличные»: {core.ft_moneybag_balance(NAL):,.0f}")


if __name__ == '__main__':
    main()
