#!/usr/bin/env python3
"""
Занос выплат Рабочих рук в Финтабло из PDF-актов самозанятых.

Использование:
    python enter_akty.py <папка_с_актами>            # dry-run: показать план, ничего не писать
    python enter_akty.py <папка_с_актами> --commit    # реально занести в Финтабло

Что делает:
  - парсит каждый PDF-акт: ФИО, ИНН, дата акта, сумма «Итого», № акта
  - сопоставляет исполнителя с контрагентом Финтабло (создаёт, если нового)
  - СТАТЬЮ ищет сперва в таблице Оли (по ФИО + сумме + месяцу) — там доп-работа размечена
    по событиям; если не нашлось, падаем на ИСТОРИЮ выплат человека, иначе — педагог с ⚠
  - дата платежа = дата акта, factMonth = месяц акта, счёт = «Рабочие руки»
  - ЗАЩИТА ОТ ЗАДВОЕНИЯ: пропускает акт, если в Финтабло уже есть выплата
    с тем же контрагентом + датой + суммой (idempotent, можно гонять повторно)
  - при --commit пишет и логирует id в exports/akty_committed_<timestamp>.json

Требует ~/Projects/finsync/.env (FINTABLO_TOKEN, FINTABLO_BASE_URL) и pdftotext (poppler).
"""
import subprocess, re, glob, json, os, sys
from collections import defaultdict, Counter

import core
import olya_sheet

RR_MONEYBAG = 167935  # счёт «Рабочие руки»
HERE = core.HERE

def norm(s): return re.sub(r'\s+', ' ', str(s or '').strip()).upper().replace('Ё', 'Е').replace(' NONE', '')
MONTHS = {'января':'01','февраля':'02','марта':'03','апреля':'04','мая':'05','июня':'06',
          'июля':'07','августа':'08','сентября':'09','октября':'10','ноября':'11','декабря':'12'}

api = core.ftmut          # общий клиент: POST без ретраев, DELETE/PUT с ретраями
get = core.ftget


def newid(res):
    return res.get('id') or (res.get('items') or [{}])[0].get('id') if isinstance(res, dict) else None

def parse_akt(path):
    txt = subprocess.run(['pdftotext', path, '-'], capture_output=True, text=True, timeout=30).stdout
    md = re.search(r'(\d{1,2})\s+(' + '|'.join(MONTHS) + r')\s+(\d{4})', txt)
    mf = re.search(r'([А-ЯЁ][а-яё]+\s+[А-ЯЁ][а-яё]+(?:\s+[А-ЯЁ][а-яё]+)?),\s*именуем', txt)
    head = txt.split('2. Вышеперечисленные')[0]
    nums = re.findall(r'(\d[\d ]*\.\d{2})', head)
    amt = float(nums[-1].replace(' ', '')) if nums else None
    aktno = (re.search(r'№\s*([\d\-]+)', txt) or [None, None])[1]
    inn = (re.search(r'_(\d{10,12})_', os.path.basename(path)) or [None, None])[1]
    if not (md and mf and amt): return None
    return {'fio': norm(mf.group(1)), 'inn': inn, 'aktno': aktno,
            'date': f"{int(md.group(1)):02d}.{MONTHS[md.group(2)]}.{md.group(3)}",
            'month': f"{MONTHS[md.group(2)]}.{md.group(3)}", 'amount': round(amt, 2),
            'service': akt_service(txt)}


def akt_service(txt):
    """Наименование услуги из акта — второй признак при разводе двойников.

    Блок между «руб., Без НДС» и «Итого:»; чистим служебные «1 ед 15000.00».
    Разбор тот же, что в ~/Projects/acts-parser/parse_acts.py.
    """
    b = re.search(r'руб\.,\s*Без НДС\s*\n(.*?)Итого:', txt, re.S)
    if not b:
        return ''
    block = re.sub(r'\d+\s*(?:ед|ч)\s+[\d  \xa0]*[\d.,]+', ' ', b.group(1))
    block = re.sub(r'^\s*\d+\s*$', ' ', block, flags=re.M)
    return ' '.join(re.findall(r'[А-Яа-яЁёA-Za-z]+', block)).strip()

def akt_description(fio, date, note=""):
    """Описание выплаты в Финтабло. Чистая функция.

    Кроме «кому и когда» несём событие из таблицы Оли («за ДЕПО 11.07»): в Финтабло сразу
    видно, за что заплатили, а матчинг план↔факт получает вторую улику помимо суммы.
    """
    base = f"Выплата {fio.title()} {date} (Рабочие руки, по акту)"
    note = re.sub(r"\s+", " ", (note or "")).strip()
    return f"{base} — {note}"[:200] if note else base


def main():
    args = [a for a in sys.argv[1:] if not a.startswith('--')]
    commit = '--commit' in sys.argv
    month = next((a.split('=', 1)[1] for a in sys.argv if a.startswith('--month=')), None)
    if not args:
        print("укажи папку с актами: python enter_akty.py <папка> [--commit]"); return
    folder = args[0]
    # рекурсивно: выгрузка «Рабочих рук» приходит деревом «Клиент/ФИО/akty/*.pdf»,
    # а не плоской папкой — по маске в корне находилось 0 актов (01.08.2026)
    files = sorted(glob.glob(os.path.join(folder, '**', '*.pdf'), recursive=True))
    print(f"актов в папке: {len(files)}  |  режим: {'ЗАПИСЬ' if commit else 'dry-run (ничего не пишу)'}\n")

    # справочники Финтабло
    parts = get('/v1/partner')['items']
    cats = {x['id']: x['name'] for x in get('/v1/category')['items']}
    name2pid = {norm(x['name']): x['id'] for x in parts}
    two2pid = {}
    for x in parts:
        t = norm(x['name']).split()
        if len(t) >= 2: two2pid.setdefault(' '.join(t[:2]), x['id'])
    pid2name = {x['id']: norm(x['name']) for x in parts}

    # история выплат РР -> статья по человеку + существующие (для дедупа).
    # Берём прошлый и текущий год: акты приносят с опозданием, но глубже история не нужна.
    tx = (core.ft_transactions(year=core.FIN_YEAR - 1, moneybagId=RR_MONEYBAG)
          + core.ft_transactions(moneybagId=RR_MONEYBAG))
    hist = defaultdict(Counter); existing = set(); existing_fio = set()
    for t in tx:
        if t.get('group') == 'outcome' and not t.get('isPlan'):
            f = pid2name.get(t.get('partnerId'))
            if f: hist[f][t.get('categoryId')] += 1
            existing.add((t.get('partnerId'), t.get('date'), round(t.get('value', 0), 2)))
            if f:  # дедуп и по ФИО — ловит повтор, когда pid ещё не разрешился (новый контрагент)
                existing_fio.add((f, t.get('date'), round(t.get('value', 0), 2)))
    histcat = {f: c.most_common(1)[0][0] for f, c in hist.items()}
    PED = next((cid for cid, nm in cats.items() if 'педагог' in nm.lower() and 'занят' in nm.lower()), None)

    # Статья из таблицы Оли — приоритетнее истории: доп-работа там размечена по событиям,
    # а история у педагога всегда перевешена уроками (см. olya_sheet.expense_index).
    # Таблица недоступна (нет сети/сломан CSV) — не роняем занос, просто работаем как раньше.
    try:
        olya_rows = olya_sheet.payment_rows(olya_sheet.fetch_rows())
        cat_by_name = olya_sheet.outcome_cats_by_name()
    except Exception as e:
        print(f"⚠ таблица Оли не прочиталась ({e}) — статьи только по истории выплат")
        olya_rows, cat_by_name = [], {}

    def olya_match(a):
        """Строка таблицы Оли под этот акт + список кандидатов, если развести не вышло."""
        return olya_sheet.match_payment(olya_rows, a['fio'], a['amount'],
                                        a['date'], a.get('service', ''))

    def pid_for(fio):
        return name2pid.get(fio) or two2pid.get(' '.join(fio.split()[:2]))

    to_add, skipped, review, failed = [], [], [], []
    for f in files:
        a = parse_akt(f)
        if not a: failed.append(os.path.basename(f)); continue
        pid = pid_for(a['fio'])
        key = (pid, a['date'], a['amount'])
        if pid and key in existing:
            skipped.append(a); continue
        if not pid and (a['fio'], a['date'], round(a['amount'], 2)) in existing_fio:
            skipped.append(a); continue
        hit, cand = olya_match(a)
        # Дата операции — «Дата платежа» из таблицы, а не дата акта: работа и платёж
        # бывают в разных месяцах (cover session 25.07 оплачивается 03.08), а касса
        # должна показывать день, когда деньги ушли.
        if hit:
            a['_src'] = 'таблица Оли'
            a['_note'] = hit.get('text') or ''
            cat, catname = cat_by_name.get(hit['cat']), hit['cat']
            a['date'], a['month'] = hit['pay_date'], hit['month']
        else:
            a['_src'] = 'история выплат'
            a['_alt'] = cand                    # >1 — двойники, которых нечем развести
            cat, catname = histcat.get(a['fio']), None
        if cat is None:
            a['_src'] = 'дефолт-педагог ⚠'
            cat = PED  # дефолт-педагог, помечаем на проверку
            review.append(a)
        a['_pid'] = pid; a['_cat'] = cat; a['_catname'] = catname or cats.get(cat, '')
        to_add.append(a)

    if month:
        before = len(to_add)
        to_add = [a for a in to_add if a['month'] == month]
        print(f"⚙ фильтр по месяцу {month}: оставлено {len(to_add)} из {before} к заносу\n")

    tot = sum(a['amount'] for a in to_add)
    print(f"К ЗАНОСУ: {len(to_add)} актов на {tot:,.0f} ₽")
    bym = defaultdict(float)
    for a in to_add: bym[a['month']] += a['amount']
    for m in sorted(bym): print(f"   {m}: {bym[m]:,.0f}")
    # Откуда взялась статья — глазами проверяемо. Доп-работа должна идти по таблице Оли
    # (событийные статьи), уроки — по истории (Зарплаты педагогов).
    from_olya = [a for a in to_add if a['_src'] == 'таблица Оли']
    if from_olya:
        print(f"\n📋 статья из таблицы Оли — {len(from_olya)} шт на {sum(a['amount'] for a in from_olya):,.0f} "
              f"(доп-работа, НЕ уроки):")
        for a in sorted(from_olya, key=lambda x: (x['date'], x['fio'])):
            print(f"   {a['date']} | {a['amount']:>9,.0f} | {a['fio'].title():<32}→ {a['_catname']}")
    by_hist = [a for a in to_add if a['_src'] == 'история выплат']
    if by_hist:
        print(f"\n📚 статья по истории выплат — {len(by_hist)} шт на {sum(a['amount'] for a in by_hist):,.0f}")
    # В таблице Оли человек есть, но статью не выбрать: несколько строк с той же суммой
    # в том же месяце. Молча упасть на историю = вернуть доп-работу в «Зарплаты педагогов».
    unsure = [a for a in to_add if len(a.get('_alt') or []) > 1]
    if unsure:
        print(f"\n❓ НЕ РАЗВЕСТИ по таблице Оли — {len(unsure)} шт. Ни дата акта, ни описание услуги "
              f"не отличили двойников; ставлю статью по истории, поправь за что в Финтабло:")
        for a in sorted(unsure, key=lambda x: (x['date'], x['fio'])):
            print(f"   {a['date']} | {a['amount']:>9,.0f} | {a['fio'].title()}")
            print(f"       акт: «{(a.get('service') or '—')[:60]}»")
            for c in a['_alt']:
                print(f"       ○ {c['pay_date']} · {c['cat']:<28} · {c['text'].strip()[:52]}")
    print(f"\nПРОПУЩЕНО как дубль по (контрагент, дата, сумма): {len(skipped)}")
    # Дедуп в актах — только по совпадению (человек, дата, сумма): externalId у актов нет.
    # Печатаем построчно, чтобы глазами поймать ложный пропуск (два разных акта в один день на одну сумму).
    for a in sorted(skipped, key=lambda x: (x['fio'], x['date'])):
        print(f"   ⏭ {a['date']} | {a['amount']:>9,.0f} | {a['fio'].title()}")
    if review: print(f"⚠ БЕЗ ИСТОРИИ СТАТЬИ (поставлю педагога, проверь): {sorted(set(a['fio'] for a in review))}")
    if failed: print(f"⚠ не распарсились: {len(failed)} — {failed[:5]}")

    bal = core.ft_moneybag_balance(RR_MONEYBAG)
    print(f"\nостаток РР сейчас {bal:,.0f} → после заноса ≈ {bal - tot:,.0f}")

    if not commit:
        print("\n(dry-run — ничего не записано. Запусти с --commit, чтобы занести.)"); return

    import time
    log = []; partner_failed = []
    for a in to_add:
        pid = a['_pid']
        if not pid:  # новый контрагент
            cr = api('POST', '/v1/partner', {'name': a['fio'].title(), 'inn': a['inn'] or ''})
            pid = newid(cr)
        if not pid:
            # Контрагент не создался — БЕЗ partnerId дедуп повторного прогона не найдёт
            # эту выплату и создаст дубль. Пропускаем этот акт, не постим транзакцию.
            print(f"  ✗ не создан контрагент {a['fio'].title()} — акт ПРОПУЩЕН (занести вручную), ответ: {cr}")
            partner_failed.append(a); continue
        body = {'value': a['amount'], 'group': 'outcome', 'moneybagId': RR_MONEYBAG, 'date': a['date'],
                'categoryId': a['_cat'], 'partnerId': pid, 'factMonth': a['month'],
                'description': akt_description(a['fio'], a['date'], a.get('_note'))}
        res = api('POST', '/v1/transaction', body); tid = newid(res)
        if tid: log.append({'id': tid, 'fio': a['fio'], 'date': a['date'], 'value': a['amount']})
        else: print("  ✗ ошибка:", a['fio'], res)
    ts = time.strftime('%Y%m%d_%H%M%S')  # реальное время запуска — второй прогон из той же папки не затрёт лог
    os.makedirs(os.path.join(HERE, 'exports'), exist_ok=True)
    out = os.path.join(HERE, 'exports', f'akty_committed_{ts}.json')
    json.dump(log, open(out, 'w'), ensure_ascii=False, indent=1)
    bal2 = core.ft_moneybag_balance(RR_MONEYBAG)
    print(f"\nзанесено: {len(log)} на {sum(x['value'] for x in log):,.0f} | остаток РР: {bal2:,.0f}")
    print(f"лог (для отката): {out}")
    if partner_failed:
        print(f"\n⚠ НЕ ЗАНЕСЕНО {len(partner_failed)} актов (не создался контрагент) — занести вручную:")
        for a in partner_failed:
            print(f"   {a['date']} | {a['amount']:>9,.0f} | {a['fio'].title()}")

if __name__ == '__main__':
    main()
