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 = "2"
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 TrueWealthActivity:
    source_row_fingerprint: str
    occurred_on: str
    event_type: str
    instrument_name: str
    isin: str
    quantity: Decimal
    gross_amount_chf: Decimal | None
    tax_amount_chf: Decimal | None


@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
    activities: tuple[TrueWealthActivity, ...] = ()

    @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_activities(text: str) -> tuple[TrueWealthActivity, ...]:
    event_labels = {
        "Kauf": "buy",
        "Verkauf": "sell",
        "Bardividende": "dividend",
        "Aktiensplit Abgang": "split_out",
        "Aktiensplit Zugang": "split_in",
    }
    activity_pattern = re.compile(
        rf"^\s*{_DATE}\s+(Aktiensplit\s+Abgang|Aktiensplit\s+Zugang|Bardividende|Verkauf|Kauf)\s+(.+)$"
    )
    current_isin: str | None = None
    current_name: str | None = None
    activities: list[TrueWealthActivity] = []
    semantic_occurrences: dict[str, int] = {}
    for raw_line in text.splitlines():
        instrument = _ISIN.match(raw_line.rstrip())
        if instrument:
            current_isin = instrument.group(1)
            current_name = instrument.group(2).rstrip(",")
            continue
        event = activity_pattern.match(raw_line.rstrip())
        if not event or not current_isin or not current_name:
            continue
        occurred_on = _iso(event.group("day"), event.group("month"), event.group("year"))
        label = re.sub(r"\s+", " ", event.group(4)).strip()
        remainder = re.sub(r"\b\d{2}\.\d{2}\.(?=\s|$)", " ", event.group(5))
        numbers = re.findall(r"[+-]?\s*[\d’']+(?:[.,]\d+)?", remainder)
        if not numbers:
            raise ValueError("TrueWealth activity quantity is missing")
        quantity = _decimal(numbers[0])
        gross: Decimal | None = None
        tax: Decimal | None = None
        if label == "Bardividende":
            if len(numbers) < 3:
                raise ValueError("TrueWealth dividend evidence is incomplete")
            gross = _decimal(numbers[2])
            tax = _decimal(numbers[3]) if len(numbers) >= 4 else None
        semantic = "|".join(
            (
                occurred_on,
                event_labels[label],
                current_isin,
                format(quantity, "f"),
                format(gross, "f") if gross is not None else "",
                format(tax, "f") if tax is not None else "",
            )
        )
        occurrence = semantic_occurrences.get(semantic, 0) + 1
        semantic_occurrences[semantic] = occurrence
        row_identity = f"{semantic}|occurrence={occurrence}"
        activities.append(
            TrueWealthActivity(
                source_row_fingerprint=sha256(row_identity.encode("utf-8")).hexdigest(),
                occurred_on=occurred_on,
                event_type=event_labels[label],
                instrument_name=current_name,
                isin=current_isin,
                quantity=quantity,
                gross_amount_chf=gross,
                tax_amount_chf=tax,
            )
        )
    if len({row.source_row_fingerprint for row in activities}) != len(activities):
        raise ValueError("TrueWealth activity rows are not uniquely identifiable")
    return tuple(activities)


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))
    exact_subtotals = [
        _decimal(value)
        for value in re.findall(
            r"(?m)^\s*Total\s+(?:Bankkonten|Wertschriften|USA)\b[^\n]*?([\d’']+(?:[.,]\d+)?)",
            text,
        )
    ]
    if exact_subtotals:
        exact_source_total = sum(exact_subtotals, Decimal("0"))
        if abs(source_total - exact_source_total) > Decimal("1.00"):
            raise ValueError("TrueWealth exact component subtotals conflict with the rounded source total")
        source_total = exact_source_total

    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,
        activities=_parse_activities(text),
    )


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)
