from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile

import pandas as pd

from .base import BaseParser, StatementMeta, finalize, norm
from .common import parse_flexible_date


_COMPACT_DATE_RE = re.compile(r"^\s*([0-3O]\d[A-Za-z]{3}\d{4})(?:\s+|$)(.*)$", re.I)
_DECIMAL_RE = re.compile(r"(?<!\d)(?:\d{1,3}(?:[,.]\d{3})*[,.]\d{2}|\d+[,.]\d{2})(?!\d)")
_SKIP_MARKERS = (
    "BALANCE CARRIED FORWARD",
    "BALANCE BROUGHT FORWARD",
    "CLOSING BALANCE",
    "TRANSACTION TURNOVER",
    "TRANSACTION COUNT",
    "WITHDRAWALS DEPOSITS",
    "DEPOSITS WITHDRAWALS",
)


def _normalise_compact_date(value: str) -> str:
    value = norm(value)
    # OCR commonly reads the leading zero as the letter O.
    if value and value[0].upper() == "O" and len(value) > 1 and value[1].isdigit():
        value = "0" + value[1:]
    parsed = parse_flexible_date(value)
    return "" if pd.isna(parsed) else parsed.strftime("%Y-%m-%d")


def _hsbc_amount(value: str | None) -> float | None:
    if value is None:
        return None
    text = str(value).strip().replace(" ", "").replace("INR", "")
    if not text or text == "-":
        return None
    # HSBC image OCR may render 84,400.00 as 84,400,00.  When no dot is
    # present and the final comma has two following digits, treat it as the
    # decimal separator and all earlier commas as thousands separators.
    if "." not in text and text.count(",") >= 1 and re.search(r",\d{2}$", text):
        head, tail = text.rsplit(",", 1)
        text = head.replace(",", "") + "." + tail
    else:
        text = text.replace(",", "")
    try:
        return float(text)
    except ValueError:
        return None


def _command_available(name: str) -> bool:
    return shutil.which(name) is not None


def _run_tesseract(image_path: Path, psm: str) -> str:
    environment = os.environ.copy()
    environment["OMP_THREAD_LIMIT"] = "1"
    result = subprocess.run(
        ["tesseract", str(image_path), "stdout", "--psm", psm, "-l", "eng"],
        capture_output=True,
        text=True,
        timeout=75,
        env=environment,
    )
    return result.stdout if result.returncode == 0 else ""


def _transaction_page_candidates(images: list[Path]) -> list[int]:
    # HSBC transaction pages contain a tall ruled table.  Detecting those
    # vertical rules is materially faster than OCRing the recurring cover,
    # legal-notice and contact pages. Pillow is already installed through
    # pdfplumber, so no additional Python dependency is introduced.
    from PIL import Image
    import numpy as np

    line_counts: list[int] = []
    for image_path in images:
        pixels = np.asarray(Image.open(image_path).convert("L"))
        height, width = pixels.shape
        y1, y2 = int(height * 0.15), int(height * 0.50)
        dark_counts = (pixels[y1:y2:2, :] < 100).sum(axis=0) * 2
        threshold = int((y2 - y1) * 0.30)
        line_counts.append(int((dark_counts > threshold).sum()))

    candidates: list[int] = []
    for index, count in enumerate(line_counts):
        previous_count = line_counts[index - 1] if index > 0 else 0
        next_count = line_counts[index + 1] if index + 1 < len(line_counts) else 0
        if count >= 8:
            candidates.append(index + 1)
        elif count >= 5 and previous_count >= 8 and next_count <= 1:
            # Short closing page at the end of a monthly statement.
            candidates.append(index + 1)
    return candidates


def _ocr_pdf(path: str | Path) -> list[tuple[int, str]]:
    if not _command_available("pdftoppm") or not _command_available("tesseract"):
        raise RuntimeError(
            "HSBC scanned-statement analysis requires the system packages "
            "poppler-utils and tesseract-ocr. Redeploy the ERP with the updated Dockerfile."
        )

    with tempfile.TemporaryDirectory(prefix="hsbc_statement_") as temp_dir:
        temp = Path(temp_dir)
        prefix = temp / "page"
        render = subprocess.run(
            [
                "pdftoppm", "-r", "110", "-gray", "-jpeg",
                "-jpegopt", "quality=70", str(path), str(prefix),
            ],
            capture_output=True,
            text=True,
            timeout=180,
        )
        if render.returncode != 0:
            raise RuntimeError(f"Unable to render HSBC PDF for OCR: {render.stderr.strip()}")

        images = sorted(temp.glob("page-*.jpg"))
        candidate_pages = _transaction_page_candidates(images)
        if not candidate_pages:
            raise ValueError("HSBC statement pages were found, but no transaction tables could be identified.")

        selected = [(page, images[page - 1]) for page in candidate_pages]
        workers = max(1, min(4, (os.cpu_count() or 2)))
        with ThreadPoolExecutor(max_workers=workers) as executor:
            texts = list(executor.map(lambda item: _run_tesseract(item[1], "6"), selected))
        return [(selected[index][0], text) for index, text in enumerate(texts)]



class HSBCParser(BaseParser):
    bank_name = "HSBC Bank"
    parser_name = "HSBCParserOCR"

    @classmethod
    def detect(cls, text: str) -> float:
        upper = (text or "").upper()
        if "HSBC" in upper and ("STATEMENT OF ACCOUNTS" in upper or "HONGKONG AND SHANGHAI" in upper):
            return 0.99
        return 0.0

    def parse(self, path: str | Path, text: str | None = None):
        # The common extraction layer now OCRs image-only and mixed PDFs for all
        # banks.  Reuse that text first.  The targeted HSBC table-page OCR remains
        # as a compatibility fallback because older deployments may call this
        # parser directly with empty text.
        supplied_text = str(text or "")
        if supplied_text.strip():
            page_texts = supplied_text.split("\f")
            pages = [(index, value) for index, value in enumerate(page_texts, start=1)]
            all_text = supplied_text
        else:
            pages = _ocr_pdf(path)
            all_text = "\n\f\n".join(page_text for _, page_text in pages)
        upper_all = all_text.upper()
        if "HSBC" not in upper_all:
            raise ValueError("The uploaded file could not be identified as an HSBC account statement.")

        meta = StatementMeta(
            bank_name=self.bank_name,
            source_file=Path(path).name,
            parser_name=self.parser_name,
            confidence="High - OCR with balance-delta reconciliation",
        )

        account_match = re.search(r"SAVINGS\s+ACCOUNT-RES\s+([0-9][0-9\- ]{6,})", all_text, re.I)
        if not account_match:
            account_match = re.search(r"ACCOUNT\s+NUMBER\s+([0-9][0-9\- ]{6,})", all_text, re.I)
        if account_match:
            meta.account_number = re.sub(r"\s+", "", account_match.group(1)).strip("-")

        ifsc_match = re.search(r"IFSC\s+CODE\s*:?\s*([A-Z0-9]+)", all_text, re.I)
        if ifsc_match:
            meta.ifsc = ifsc_match.group(1).upper()

        customer_match = re.search(r"\b(MS|MRS|MR|M/S)\s+([A-Z][A-Z .]{3,60})\n", all_text)
        if customer_match:
            meta.customer_name = norm(f"{customer_match.group(1)} {customer_match.group(2)}").title()

        rows: list[dict] = []
        current_date = ""
        narration_parts: list[str] = []
        opening_balance: float | None = None
        last_closing: float | None = None

        for page_number, page_text in pages:
            page_upper = page_text.upper()
            is_transaction_page = (
                "BALANCE BROUGHT FORWARD" in page_upper
                or "BALANCE CARRIED FORWARD" in page_upper
                or "CLOSING BALANCE" in page_upper
            ) and ("SAVINGS ACCOUNT" in page_upper or "TRANSACTION TURNOVER" in page_upper)
            if not is_transaction_page:
                continue

            for raw_line in page_text.splitlines():
                line = norm(raw_line)
                if not line:
                    continue
                upper = line.upper()

                date_match = _COMPACT_DATE_RE.match(line)
                if date_match:
                    parsed_date = _normalise_compact_date(date_match.group(1))
                    if parsed_date:
                        current_date = parsed_date
                    line = norm(date_match.group(2))
                    upper = line.upper()

                if "BALANCE BROUGHT FORWARD" in upper:
                    amounts = [_hsbc_amount(m.group(0)) for m in _DECIMAL_RE.finditer(line)]
                    amounts = [value for value in amounts if value is not None]
                    if amounts and opening_balance is None:
                        opening_balance = amounts[-1]
                    narration_parts = []
                    continue

                if "BALANCE CARRIED FORWARD" in upper:
                    narration_parts = []
                    continue

                if "CLOSING BALANCE" in upper:
                    amounts = [_hsbc_amount(m.group(0)) for m in _DECIMAL_RE.finditer(line)]
                    amounts = [value for value in amounts if value is not None]
                    if amounts:
                        last_closing = amounts[-1]
                    narration_parts = []
                    continue

                if any(marker in upper for marker in ("TRANSACTION TURNOVER", "TRANSACTION COUNT")):
                    narration_parts = []
                    continue

                if upper.startswith(("DATE ", "DETAILS ", "DEPOSITS ", "WITHDRAWALS ", "PAGE ")):
                    continue
                if "THE HONGKONG AND SHANGHAI BANKING" in upper or "WWW.HSBC" in upper:
                    narration_parts = []
                    continue

                amount_matches = list(_DECIMAL_RE.finditer(line))
                values = [_hsbc_amount(match.group(0)) for match in amount_matches]
                values = [value for value in values if value is not None]

                if len(values) >= 2 and current_date:
                    transaction_amount = values[-2]
                    balance = values[-1]
                    text_before_amount = line[: amount_matches[-2].start()].strip()
                    if text_before_amount:
                        narration_parts.append(text_before_amount)
                    narration = norm(" ".join(narration_parts))
                    if narration and not any(marker in narration.upper() for marker in _SKIP_MARKERS):
                        reference_match = re.search(r"\b(?:UPI|HIB|CNRBR|SBIN|ICIC|HSBCN)?[A-Z0-9]{10,}\b", narration, re.I)
                        rows.append(
                            {
                                "transaction_date": current_date,
                                "value_date": current_date,
                                "narration": narration,
                                "reference_no": reference_match.group(0) if reference_match else "",
                                # The shared delta engine in base.finalize() assigns
                                # the correct side.  Seed as debit so the original
                                # OCR amount remains available as printed_debit.
                                "debit": transaction_amount,
                                "credit": None,
                                "balance": balance,
                                "source_page": page_number,
                            }
                        )
                    narration_parts = []
                else:
                    # Ignore table labels and footer debris while preserving the
                    # multi-line HSBC narration and references.
                    if not any(marker in upper for marker in _SKIP_MARKERS):
                        narration_parts.append(line)

        if not rows:
            raise ValueError(
                "HSBC statement was identified, but no transaction rows could be extracted. "
                "Verify that the PDF pages are readable and redeploy with OCR dependencies."
            )

        meta.opening_balance = opening_balance
        meta.closing_balance = last_closing
        frame = finalize(pd.DataFrame(rows), meta)
        if not frame.empty:
            valid_dates = frame["transaction_date"].dropna()
            if not valid_dates.empty:
                meta.period_from = valid_dates.min().strftime("%Y-%m-%d")
                meta.period_to = valid_dates.max().strftime("%Y-%m-%d")
            meta.total_debit = round(float(frame["debit"].fillna(0).sum()), 2)
            meta.total_credit = round(float(frame["credit"].fillna(0).sum()), 2)
            if meta.closing_balance is None and pd.notna(frame["balance"].iloc[-1]):
                meta.closing_balance = float(frame["balance"].iloc[-1])
        return meta, frame
