from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from hashlib import sha256
from io import BytesIO
import re


PARSER_ID = "truewealth_tax_statement"
PARSER_VERSION = "1"
MAX_PDF_BYTES = 5 * 1024 * 1024
_DATE = r"(?P<day>\d{2})\.(?P<month>\d{2})\.(?P<year>\d{4})"
_DATE_PLAIN = r"\d{2}\.\d{2}\.\d{4}"
_ISIN = re.compile(r"^\s*\d+\s+([A-Z]{2}[A-Z0-9]{9}[0-9])\s+-\s+(.+?)\s*$")
_POSITION = re.compile(
    rf"^\s*{_DATE}\s+Bestand\s*/\s*Steuerwert\s*/\s*Ertrag\s+"
    r"([\d’'.,]+)\s+([A-Z]{3})\s+([\d’'.,]+)\s+([\d’'.,]+)(?:\s|$)"
)
_CASH_LABEL = re.compile(r"^\s*(CHF|EUR|GBP|USD)\s+\(")
_CASH_VALUE = re.compile(
    rf"^\s*{_DATE}\s+Steuerwert\s*/\s*Ertrag\s+([A-Z]{{3}})\s+(.+)$"
)


@dataclass(frozen=True)
class TrueWealthPosition:
    source_ref: str
    name: str
    isin: str
    quantity: Decimal
    currency: str
    source_price: Decimal
    source_value_chf: Decimal


@dataclass(frozen=True)
class TrueWealthCash:
    source_ref: str
    currency: str
    amount_original: Decimal
    fx_rate_to_chf: Decimal | None
    value_chf: Decimal


@dataclass(frozen=True)
class TrueWealthStatement:
    statement_date: str
    period_from: str
    period_to: str
    source_total_chf: Decimal
    positions: tuple[TrueWealthPosition, ...]
    cash: tuple[TrueWealthCash, ...]
    file_sha256: str
    page_count: int

    @property
    def securities_total_chf(self) -> Decimal:
        return sum((row.source_value_chf for row in self.positions), Decimal("0"))

    @property
    def cash_total_chf(self) -> Decimal:
        return sum((row.value_chf for row in self.cash), Decimal("0"))

    @property
    def components_total_chf(self) -> Decimal:
        return self.securities_total_chf + self.cash_total_chf


def _iso(day: str, month: str, year: str) -> str:
    return f"{year}-{month}-{day}"


def _decimal(value: str) -> Decimal:
    normalized = value.replace("’", "").replace("'", "").replace(" ", "")
    if normalized.count(",") == 1 and "." not in normalized:
        normalized = normalized.replace(",", ".")
    try:
        return Decimal(normalized)
    except InvalidOperation as exc:
        raise ValueError("TrueWealth statement contains an invalid numeric value") from exc


def _extract_pdf_text(raw: bytes) -> tuple[str, int]:
    if not raw or len(raw) > MAX_PDF_BYTES or not raw.startswith(b"%PDF-"):
        raise ValueError("TrueWealth source must be a bounded PDF")
    try:
        from pypdf import PdfReader

        reader = PdfReader(BytesIO(raw), strict=True)
        if reader.is_encrypted:
            raise ValueError("Encrypted TrueWealth PDFs are not supported")
        pages = [page.extract_text(extraction_mode="layout") or "" for page in reader.pages]
    except ValueError:
        raise
    except Exception as exc:
        raise ValueError("TrueWealth PDF could not be read") from exc
    if not pages or not all(page.strip() for page in pages[:6]):
        raise ValueError("TrueWealth PDF does not contain the expected text layer")
    return "\n\f\n".join(pages), len(pages)


def parse_truewealth_text(text: str, *, file_sha256: str = "synthetic", page_count: int = 7) -> TrueWealthStatement:
    if "True Wealth AG" not in text or "Steuerauszug in CHF" not in text:
        raise ValueError("PDF is not an evidenced TrueWealth tax statement")
    date_match = re.search(rf"Steuerauszug\s+in\s+CHF\s+{_DATE}", text)
    period_match = re.search(rf"Periode\s+{_DATE_PLAIN}\s*-\s*{_DATE_PLAIN}", text)
    if not date_match or not period_match:
        raise ValueError("TrueWealth statement date or period is missing")
    statement_date = _iso(date_match.group("day"), date_match.group("month"), date_match.group("year"))
    period_dates = re.findall(r"\d{2}\.\d{2}\.\d{4}", period_match.group(0))
    period_from = "-".join(reversed(period_dates[0].split(".")))
    period_to = "-".join(reversed(period_dates[1].split(".")))
    if statement_date != period_to:
        raise ValueError("TrueWealth statement date conflicts with the period end")

    total_match = re.search(
        rf"Total\s+Steuerwert\s+der[\s\S]{{0,500}}?Werte\s+am\s+{re.escape(period_dates[1])}[\s\S]{{0,350}}?\n\s*([\d’']+(?:[.,]\d+)?)\s+",
        text,
    )
    if not total_match:
        raise ValueError("TrueWealth source total is missing")
    source_total = _decimal(total_match.group(1))

    positions: list[TrueWealthPosition] = []
    cash: list[TrueWealthCash] = []
    current_isin: str | None = None
    current_name: list[str] = []
    current_cash_currency: str | None = None
    for raw_line in text.splitlines():
        line = raw_line.rstrip()
        cash_label = _CASH_LABEL.match(line)
        if cash_label:
            current_cash_currency = cash_label.group(1)
            continue
        cash_value = _CASH_VALUE.match(line)
        if cash_value and current_cash_currency:
            currency = cash_value.group(4)
            if currency != current_cash_currency:
                raise ValueError("TrueWealth cash currency is inconsistent")
            numbers = re.findall(r"[\d’'.,]+", cash_value.group(5))
            required = 2 if currency == "CHF" else 3
            if len(numbers) < required:
                raise ValueError("TrueWealth cash value is incomplete")
            amount = _decimal(numbers[0])
            optional_fx = None if currency == "CHF" else _decimal(numbers[1])
            value_chf = _decimal(numbers[1] if currency == "CHF" else numbers[2])
            cash.append(
                TrueWealthCash(
                    source_ref=f"cash:{currency}",
                    currency=currency,
                    amount_original=amount,
                    fx_rate_to_chf=optional_fx,
                    value_chf=value_chf,
                )
            )
            current_cash_currency = None
            continue

        start = _ISIN.match(line)
        if start:
            current_isin = start.group(1)
            current_name = [start.group(2).strip()]
            continue
        if current_isin and line.strip() and not re.match(r"^\s*\d{2}\.\d{2}\.\d{4}", line):
            if not any(token in line for token in ("True Wealth AG", "Kunde", "Kdnr.", "Periode", "Erstellt am", "Kanton", "Valoren-Nr.", "Datum", "ISIN", "Depot ", "USA", "Werte mit Anrechnung")):
                current_name.append(line.strip())
            continue
        position = _POSITION.match(line)
        if position and current_isin:
            row_date = _iso(position.group("day"), position.group("month"), position.group("year"))
            if row_date != statement_date:
                raise ValueError("TrueWealth position date conflicts with statement date")
            positions.append(
                TrueWealthPosition(
                    source_ref=f"position:{current_isin}",
                    name=" ".join(current_name),
                    isin=current_isin,
                    quantity=_decimal(position.group(4)),
                    currency=position.group(5),
                    source_price=_decimal(position.group(6)),
                    source_value_chf=_decimal(position.group(7)),
                )
            )
            current_isin = None
            current_name = []

    if len(positions) < 1 or len({row.isin for row in positions}) != len(positions):
        raise ValueError("TrueWealth positions are missing or duplicated")
    if not cash:
        raise ValueError("TrueWealth cash rows are missing")
    if any(row.currency != "CHF" for row in positions):
        raise ValueError("TrueWealth tax-statement position values must be reported in CHF")
    difference = source_total - (sum((row.source_value_chf for row in positions), Decimal("0")) + sum((row.value_chf for row in cash), Decimal("0")))
    if abs(difference) > Decimal("1.00"):
        raise ValueError("TrueWealth components do not reconcile to the source total")
    return TrueWealthStatement(
        statement_date=statement_date,
        period_from=period_from,
        period_to=period_to,
        source_total_chf=source_total,
        positions=tuple(positions),
        cash=tuple(cash),
        file_sha256=file_sha256,
        page_count=page_count,
    )


def parse_truewealth_tax_statement(raw: bytes) -> TrueWealthStatement:
    text, pages = _extract_pdf_text(raw)
    return parse_truewealth_text(text, file_sha256=sha256(raw).hexdigest(), page_count=pages)
