# -*- coding: utf-8 -*-
"""
PDF‑парсер АТ «СЕНС БАНК»  v2.8  (2025‑07‑28)

• приход/расход определяется строго по заголовкам
  «Реєстр документів за дебетом / кредитом»;
• из блока «Кореспондент …» вытягивает
    ─ юр‑наименования вида  (ТОВ|ТЗОВ|…) "Название"
    ─ а также любые другие выражения без кавычек;
• корректно парсит:
      №39  →  ТОВ "Професійні консультаційні послуги для бізнесу"
      №902532 →  БАНКНОТИ ТА МОНЕТИ В КАСІ ВІД"ЛЬВІСЬКЕ"
• если в призначенні есть «комісія XX,YYгрн», создаётся
  отдельная расходная транзакция‑комиссия;
• двойные кавычки удваиваются под формат 1С‑ClientBank;
• убирает дубли по (№, дата‑время, |amount|);
• подробный DEBUG‑лог (_grab_counterparty) помогает отслеживать,
  что именно «съедает» требуемый текст.
"""

from __future__ import annotations

import logging
import re
from datetime import datetime
from typing import List, Optional, Tuple

import pdfplumber

from app.models.transaction import Transaction
from app.parsers.base_parser import BaseBankStatementParser

# ──────────────────────── логирование ───────────────────────
log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())
#
# logging.basicConfig(level=logging.DEBUG, format="%(levelname)s:%(message)s")
# ────────────────────────────────────────────────────────────

# ───────────── сервисные regex/вспом. функции ─────────────
_RE_DOC_HDR  = re.compile(r"Док\.?\s*№\s*(\d+)", re.I)
_RE_BLOCK_DT = re.compile(r"Дата\s+(\d{2}\.\d{2}\.\d{4})", re.I)
_RE_AMOUNT   = re.compile(r"Сума\s+([\d\u00A0\u202F ]+,\d{2})", re.I)
_RE_NUM      = re.compile(r"\b\d[\d\u00A0\u202F ]*,\d{2}\b")

_RE_IBAN_T   = re.compile(r"\bUA\d{25}\b")
_RE_IBAN_S   = re.compile(r"\bUA[\d\u00A0\u202F ]{25,}\b")
_RE_INN      = re.compile(r"\b\d{8,10}\b")
_RE_LONGDIG  = re.compile(r"\b\d{7,}\b")

# --- комиссии ---
_RE_COMMISSION = re.compile(
    r"""коміс\w*?       
        \s+                
        (?P<amount>        
            [\d\u00A0\u202F]+   
            [\.,]\d{2}          
        )
        \s*грн
    """,
    re.I | re.X,
)

_SPACES  = re.compile(r"[ \u00A0\u202F]")
to_float = lambda s: float(_SPACES.sub("", s).replace(",", "."))

_LEGAL_PREFIXES = ("ТОВ", "ТЗОВ", "TЗОВ", "ПП", "АТ", "ФОП")

# «кавычные» названия ‑ ТОВ "….."
_RE_LEGAL_NAME_Q = re.compile(
    rf"\b({'|'.join(_LEGAL_PREFIXES)})\s+\"[^\"\.]+?\"(?![\w\"])",
    re.I | re.S,
)

# Любое словосочетание (≥ 10 символов) без кавычек: хватит для «БАНКНОТИ …»
_RE_LEGAL_NAME_ANY = re.compile(r"[А-ЯA-ZЇІЄҐ0-9][\w\s\"'‑–\-,\.«»]{10,}", re.I)


def esc_1c(s: str) -> str:
    """Дублирует двойные кавычки под формат 1С‑ClientBank."""
    return s.replace('"', '""')


# ───────────────────── основной класс ───────────────────────
class SenseBankPdfParser(BaseBankStatementParser):
    """Парсер PDF‑выписок АТ «СЕНС БАНК»."""

    # ================ PUBLIC API =================
    def parse(self, file_path: str) -> List[Transaction]:
        lines = self._read_pdf(file_path)
        self._init_header(lines[:40])
        blocks = self._split_blocks(lines)

        txs, seen = [], set()
        for ledger, blk in blocks:
            for t in self._parse_block(ledger, blk) or []:
                key = (t.number, t.date.strftime("%d.%m.%Y %H:%M"), abs(t.amount))
                if key not in seen:
                    seen.add(key)
                    txs.append(t)
        return txs

    # ================ TECH ========================
    @staticmethod
    def _read_pdf(path: str) -> List[str]:
        out: List[str] = []
        with pdfplumber.open(path) as pdf:
            for page in pdf.pages:
                txt = page.extract_text() or ""
                out.extend(line.strip() for line in txt.splitlines() if line.strip())
        return out

    def _init_header(self, head: List[str]):
        hdr = "\n".join(head)
        self.our_company_name = esc_1c((re.search(r'(ТОВ|ФОП)\s+"[^"]+"', hdr) or [""])[0])
        self.our_company_account = (_RE_IBAN_T.search(hdr) or [""])[0]
        self.our_company_inn = (_RE_INN.search(hdr) or [""])[0]
        self._bank_name = 'АТ ""СЕНС БАНК""'    # пригодится для комиссий

    def _split_blocks(self, lines: List[str]) -> List[Tuple[str, List[str]]]:
        """
        Розбиває весь текст виписки на окремі «блоки документа»,
        прив’язуючи кожен блок до поточного реєстру
        (DEBIT ‑ списання, CREDIT ‑ надходження).

        Алгоритм:
        • якщо зустріли заголовок реєстру — спершу зберігаємо вже
          накопичений блок (якщо він є), потім перемикаємо cur_ledger;
        • якщо зустріли рядок «Док. № …» — так само спершу закриваємо
          попередній блок, а потім починаємо новий;
        • усі інші рядки просто додаємо до поточного блока.
        """
        blocks: list[tuple[str, list[str]]] = []
        cur: list[str] = []
        cur_ledger: Optional[str] = None  # "DEBIT" | "CREDIT"

        for ln in lines:
            # --- заголовок реєстру -----------------------------------------
            if "Реєстр документів за дебетом" in ln:
                if cur and cur_ledger:  # закриваємо поточний блок
                    blocks.append((cur_ledger, cur))
                    cur = []
                cur_ledger = "DEBIT"
                continue

            if "Реєстр документів за кредитом" in ln:
                if cur and cur_ledger:
                    blocks.append((cur_ledger, cur))
                    cur = []
                cur_ledger = "CREDIT"
                continue

            # --- початок нового документа ----------------------------------
            if _RE_DOC_HDR.search(ln):
                if cur and cur_ledger:
                    blocks.append((cur_ledger, cur))
                cur = [ln]  # стартуємо новий блок
                continue

            # --- звичайний текст усередині блока ---------------------------
            if cur:
                cur.append(ln)

        # «хвіст» файлу
        if cur and cur_ledger:
            blocks.append((cur_ledger, cur))

        return blocks

    # ───────────────────────── ONE BLOCK ─────────────────────────
    def _parse_block(self, ledger_hint: str, blk: List[str]) -> Optional[List[Transaction]]:
        """
        Возвращает список транзакций:
            • основная (приход / расход)
            • отдельная транзакция‑комиссия (если в назначении есть «комісія … грн»)
        """
        m_num = _RE_DOC_HDR.search(blk[0])
        m_dt = _RE_BLOCK_DT.search(blk[0])
        if not (m_num and m_dt):
            return None

        num = m_num.group(1)
        dt = datetime.strptime(m_dt.group(1), "%d.%m.%Y")

        # ── сумма документа ──────────────────────────────────────
        joined = " ".join(blk)
        if (m := _RE_AMOUNT.search(joined)):
            amt = to_float(m.group(1))
        elif (m := _RE_NUM.search(joined)):
            amt = to_float(m.group())
        else:
            return None  # ← без суммы ‑ пропускаем

        # ── контрагент + назначение ──────────────────────────────
        payer_name, payer_acc, payer_inn = self._grab_counterparty(blk)
        purpose_raw = self._grab_purpose(blk)

        # ── дебет / кредит ───────────────────────────────────────
        is_debit = ledger_hint == "DEBIT"
        if is_debit:  # списание
            amt_signed = -amt
            payer_name_main, payer_inn_main, payer_acc_main = self.our_company_name, self.our_company_inn, self.our_company_account
            recip_name_main, recip_inn_main, recip_acc_main = payer_name, payer_inn, payer_acc
            d_out_main, d_in_main = dt, None
        else:  # поступление
            amt_signed = amt
            payer_name_main, payer_inn_main, payer_acc_main = payer_name, payer_inn, payer_acc
            recip_name_main, recip_inn_main, recip_acc_main = self.our_company_name, self.our_company_inn, self.our_company_account
            d_out_main, d_in_main = None, dt

        txs: List[Transaction] = [
            Transaction(
                number=num, date=dt, amount=amt_signed,
                payment_details=esc_1c(purpose_raw),
                payer_name=payer_name_main, payer_inn=payer_inn_main, payer_account=payer_acc_main,
                recipient_name=recip_name_main, recipient_inn=recip_inn_main, recipient_account=recip_acc_main,
                date_outcome=d_out_main, date_income=d_in_main,
            )
        ]

        # ── комиссионная транзакция (только для CREDIT) ──────────
        commission = self._extract_commission(purpose_raw)
        if commission is not None and not is_debit:
            bank_inn = payer_inn or getattr(self, "_bank_inn", "")  # ИНН банка: из блока или «хардкод»
            txs.append(
                Transaction(
                    number=f"{num}-FEE", date=dt, amount=-commission,
                    payment_details=esc_1c(f"Комісія за еквайрінг ({commission:.2f} грн)"),
                    payer_name=self.our_company_name, payer_inn=self.our_company_inn,
                    payer_account=self.our_company_account,
                    recipient_name=self._bank_name, recipient_inn=bank_inn, recipient_account="",
                    date_outcome=dt, date_income=None,
                )
            )

        return txs

    # ──────────────── COMMISSION PARSER ────────────────
    @staticmethod
    def _extract_commission(purpose: str) -> Optional[float]:
        """
        Ищет в строке конструкцию вида «комісія … грн» и возвращает
        сумму как float, либо **None**, если комиссии нет.

        • нормализует все «нестандартные» пробелы + переносы строк;
        • не чувствителен к регистру/вариантам написания «коміс*»;
        • если в назначении встречается несколько «комісія …», берётся первая.
        """
        # NBSP,NNBSP,табуляции и переводы строк → обычные пробелы
        canon = re.sub(r"[ \u00A0\u202F\t\r\n]+", " ", purpose)

        m = _RE_COMMISSION.search(canon)
        if not m:
            return None

        raw = m.group("amount").replace(",", ".")  # «83,19» → «83.19»
        try:
            return float(raw)
        except ValueError:
            log.warning("Bad commission format: %r", raw)
            return None

    # ================ COUNTER‑PARTY (v2.8) =========
    def _grab_counterparty(self, blk: List[str]) -> Tuple[str, str, str]:
        """
        Возвращает (name, iban, inn) + DEBUG‑лог.
        Анализируем также до 3‑х строк *перед* маркером «Кореспондент».
        """
        acc = inn = ""
        candidates: List[str] = []

        for i, ln in enumerate(blk):
            if not acc and (m := _RE_IBAN_T.search(ln)):
                acc = m.group()
            if not inn and (m := _RE_INN.search(ln)):
                inn = m.group()

            if "Кореспондент" not in ln:
                continue

            # ---------- собираем 1‑3 строки ДО маркера ----------
            before: List[str] = []
            k = i - 1
            while k >= 0 and len(before) < 3 \
                    and not _RE_DOC_HDR.search(blk[k]) \
                    and not blk[k].startswith(("Дата", "Док.", "Сума", "Призн.", "Банк")):
                before.insert(0, blk[k])
                k -= 1
            raw_before = " ".join(before)

            # ---------- «Кореспондент …» ----------
            after = [ln.split("Кореспондент", 1)[1]]
            j = i + 1
            while j < len(blk) \
                    and not blk[j].startswith(("Банк", "Призн.", "Проведений")) \
                    and not _RE_DOC_HDR.search(blk[j]):
                after.append(blk[j])
                j += 1
            raw_after = " ".join(after)

            raw_ctx = f"{raw_before} {raw_after}".strip()
            log.debug("[CP] CTX   → %s", raw_ctx)

            # ---------- зачистка ----------
            clean = re.sub(r"Ід\W*код\s+\d{8,10}", "", raw_ctx, flags=re.I)
            clean = _RE_IBAN_S.sub("", clean)
            clean = _RE_LONGDIG.sub("", clean)
            clean = re.sub(r"\s{2,}", " ", clean).strip(" ,–")
            log.debug("[CP] CLEAN → %s", clean)

            name = ""

            if (m := _RE_LEGAL_NAME_Q.search(clean)):
                name = m.group(0)
            elif (m := _RE_LEGAL_NAME_ANY.search(clean)):
                name = m.group(0).strip()
            else:
                for pref in _LEGAL_PREFIXES:
                    if (p := re.search(rf"\b{pref}\b", clean, re.I)):
                        name = clean[p.start():].strip()
                        break

            if name:
                name = esc_1c(name)
                candidates.append(name)
                log.debug("[CP] CAND  → %s", name)

        for cand in reversed(candidates):
            if not re.search(r"сенс\s+банк", cand, re.I):
                log.debug("[CP] PICK  → %s", cand)
                return cand, acc, inn

        fb = candidates[-1] if candidates else self._bank_name
        log.debug("[CP] PICK  → %s (fallback)", fb)
        return fb, acc, inn

    # ================ PURPOSE =====================
    @staticmethod
    def _grab_purpose(blk: List[str]) -> str:
        for k, ln in enumerate(blk):
            if "Призн." in ln:
                buf = [ln.split("Призн.", 1)[1]]
                z = k + 1
                while z < len(blk) \
                        and not blk[z].startswith(("Проведений",)) \
                        and not _RE_DOC_HDR.search(blk[z]):
                    buf.append(blk[z])
                    z += 1
                return " ".join(buf).strip()
        return ""
