from __future__ import annotations

import re
import zipfile
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import Iterable
from xml.etree import ElementTree as ET

from .broker_mapping import normalize_name


@dataclass(frozen=True)
class ParsedBrokerRow:
    source_platform: str
    source_file_type: str
    source_row_ref: str
    source_label: str
    detected_asset_class: str
    detected_currency: str | None = None
    detected_quantity_present: bool = False
    detected_market_value_present: bool = False
    account_label: str | None = None
    isin: str | None = None
    ticker: str | None = None
    exchange: str | None = None
    quality_flags: tuple[str, ...] = field(default_factory=tuple)

    @property
    def normalized_name(self) -> str:
        return normalize_name(self.source_label)


@dataclass(frozen=True)
class ParsedBrokerFile:
    source_platform: str
    source_file_type: str
    rows_total: int
    candidates: tuple[ParsedBrokerRow, ...]
    detected_snapshot_date: str | None
    snapshot_date_status: str
    detected_sections: tuple[str, ...]
    warnings: tuple[str, ...] = field(default_factory=tuple)
    errors: tuple[str, ...] = field(default_factory=tuple)

    @property
    def candidate_positions(self) -> int:
        return sum(1 for r in self.candidates if r.detected_asset_class in {"equity", "etf", "other"})

    @property
    def candidate_cash_rows(self) -> int:
        return sum(1 for r in self.candidates if r.detected_asset_class == "cash")

    @property
    def quality_flags_summary(self) -> dict[str, int]:
        return dict(Counter(flag for row in self.candidates for flag in row.quality_flags))


def _cell_text(cell: ET.Element) -> str:
    texts = [t.text or "" for t in cell.iter() if t.tag.endswith('}t') or t.tag == 't']
    return " ".join(x.strip() for x in texts if x and x.strip()).strip()


def _docx_tables(path: Path) -> list[list[list[str]]]:
    with zipfile.ZipFile(path) as zf:
        xml = zf.read("word/document.xml")
    root = ET.fromstring(xml)
    tables: list[list[list[str]]] = []
    for tbl in [e for e in root.iter() if e.tag.endswith('}tbl')]:
        rows: list[list[str]] = []
        for tr in [e for e in tbl if e.tag.endswith('}tr')]:
            rows.append([_cell_text(tc) for tc in tr if tc.tag.endswith('}tc')])
        if rows:
            tables.append(rows)
    return tables


def _xlsx_rows(path: Path) -> list[list[str]]:
    with zipfile.ZipFile(path) as zf:
        shared: list[str] = []
        if "xl/sharedStrings.xml" in zf.namelist():
            ss = ET.fromstring(zf.read("xl/sharedStrings.xml"))
            for si in ss.iter():
                if si.tag.endswith('}si'):
                    shared.append(" ".join((t.text or "").strip() for t in si.iter() if t.tag.endswith('}t')).strip())
        sheet_name = next(n for n in zf.namelist() if n.startswith("xl/worksheets/sheet") and n.endswith(".xml"))
        root = ET.fromstring(zf.read(sheet_name))
    rows: list[list[str]] = []
    for row in [e for e in root.iter() if e.tag.endswith('}row')]:
        vals: list[str] = []
        for c in [e for e in row if e.tag.endswith('}c')]:
            v = next((x for x in c if x.tag.endswith('}v')), None)
            value = v.text if v is not None and v.text is not None else ""
            if c.attrib.get('t') == 's' and value:
                value = shared[int(value)]
            vals.append(value)
        if any(vals):
            rows.append(vals)
    return rows


def _date_status(texts: Iterable[str]) -> tuple[str | None, str]:
    joined = " ".join(texts)
    m = re.search(r"(\d{2}\.\d{2}\.\d{4}|\d{4}-\d{2}-\d{2})", joined)
    if not m:
        return None, "missing"
    return m.group(1), "clear"


def _currency(cells: Iterable[str]) -> str | None:
    text = " ".join(cells).upper()
    for cur in ("CHF", "USD", "EUR", "GBP"):
        if cur in text:
            return cur
    return None


def _is_cash(cells: list[str]) -> bool:
    text = " ".join(cells).lower()
    return any(k in text for k in ["cash", "konto", "liquidität", "barbestand", "privatkonto"])


def _has_amount_marker(cells: list[str], *markers: str) -> bool:
    text = " ".join(cells).lower()
    return any(m in text for m in markers)


def parse_postfinance_docx(path: Path) -> ParsedBrokerFile:
    tables = _docx_tables(path)
    flat = [cell for table in tables for row in table for cell in row]
    date, status = _date_status(flat)
    candidates: list[ParsedBrokerRow] = []
    for ti, table in enumerate(tables, start=1):
        for ri, cells in enumerate(table[1:] if table else [], start=2):
            label = cells[0].strip() if cells else ""
            if not label or label.lower() in {"produkt", "name"}:
                continue
            cur = _currency(cells)
            is_cash = _is_cash(cells)
            flags = ["needs_manual_review"]
            if is_cash:
                flags.append("snapshot_only")
                asset = "cash"
            else:
                asset = "etf" if "etf" in label.lower() or "fund" in label.lower() else "equity"
                flags += ["missing_isin", "missing_ticker", "snapshot_only", "cost_basis_uncertain"]
                if _has_amount_marker(cells, "marktwert", "market value"):
                    flags.append("market_value_legacy")
            candidates.append(ParsedBrokerRow("PostFinance", "docx", f"table{ti}:row{ri}", label, asset, cur, _has_amount_marker(cells, "menge", "quantity", "stück"), _has_amount_marker(cells, "marktwert", "market value"), "PostFinance Depot", quality_flags=tuple(dict.fromkeys(flags))))
    return ParsedBrokerFile("PostFinance", "docx", sum(len(t) for t in tables), tuple(candidates), date, status, tuple(f"table_{i}" for i in range(1, len(tables)+1)))


def parse_true_wealth_docx(path: Path) -> ParsedBrokerFile:
    tables = _docx_tables(path)
    flat = [cell for table in tables for row in table for cell in row]
    date, status = _date_status(flat)
    candidates: list[ParsedBrokerRow] = []
    for ti, table in enumerate(tables, start=1):
        for ri, cells in enumerate(table[1:] if table else [], start=2):
            label = cells[0].strip() if cells else ""
            if not label:
                continue
            cur = _currency(cells)
            is_cash = _is_cash(cells)
            flags = ["needs_manual_review", "snapshot_only"]
            asset = "cash" if is_cash else "etf"
            if not is_cash:
                flags += ["missing_isin", "missing_ticker", "cost_basis_uncertain"]
            if cur and cur != "CHF":
                flags.append("missing_fx")
            candidates.append(ParsedBrokerRow("True Wealth", "docx", f"table{ti}:row{ri}", label, asset, cur, _has_amount_marker(cells, "anteile", "quantity", "menge"), _has_amount_marker(cells, "wert", "value"), "True Wealth Portfolio", quality_flags=tuple(dict.fromkeys(flags))))
    return ParsedBrokerFile("True Wealth", "docx", sum(len(t) for t in tables), tuple(candidates), date, status, tuple(f"table_{i}" for i in range(1, len(tables)+1)))


def parse_raiffeisen_xlsx(path: Path) -> ParsedBrokerFile:
    rows = _xlsx_rows(path)
    date, status = _date_status(cell for row in rows for cell in row)
    candidates: list[ParsedBrokerRow] = []
    for ri, cells in enumerate(rows[1:] if rows else [], start=2):
        label = cells[0].strip() if cells else ""
        if not label:
            continue
        cur = _currency(cells)
        is_cash = _is_cash(cells)
        flags = ["needs_manual_review"]
        if is_cash:
            flags += ["cash_snapshot_candidate"]
            asset = "cash"
        else:
            flags += ["aggregate_only", "missing_instrument_details"]
            asset = "other"
        candidates.append(ParsedBrokerRow("Raiffeisen", "xlsx", f"sheet1:row{ri}", label, asset, cur, False, _has_amount_marker(cells, "wert", "saldo", "betrag"), "Raiffeisen Übersicht", quality_flags=tuple(dict.fromkeys(flags))))
    return ParsedBrokerFile("Raiffeisen", "xlsx", len(rows), tuple(candidates), date, status, ("sheet_1",))


def parse_broker_file(source_platform: str, path: Path) -> ParsedBrokerFile:
    platform = source_platform.lower().replace(" ", "")
    suffix = path.suffix.lower()
    if platform == "postfinance" and suffix == ".docx":
        return parse_postfinance_docx(path)
    if platform == "truewealth" and suffix == ".docx":
        return parse_true_wealth_docx(path)
    if platform == "raiffeisen" and suffix == ".xlsx":
        return parse_raiffeisen_xlsx(path)
    raise ValueError(f"Unsupported broker parser combination: {source_platform} {suffix}")
