from __future__ import annotations

import csv
from dataclasses import dataclass
from io import StringIO
from sqlite3 import Connection
from typing import Any

from jarvis_finance.services.budget_imports import (
    detect_duplicate_candidates,
    seed_bank_transfer_candidates_from_rows,
    seed_credit_card_candidates_from_rows,
    seed_migros_candidates_from_rows,
)
from jarvis_finance.services.household_source_analysis import parse_raiffeisen_physical_records


@dataclass(frozen=True)
class ImportProfile:
    name: str
    source_type: str
    filename_patterns: tuple[str, ...]
    delimiter: str
    encoding: str
    date_columns: tuple[str, ...]
    booking_date_columns: tuple[str, ...]
    description_columns: tuple[str, ...]
    amount_columns: tuple[str, ...]
    currency_columns: tuple[str, ...]
    account_columns: tuple[str, ...]
    fingerprint_columns: tuple[str, ...]
    default_rules: tuple[str, ...]


IMPORT_PROFILES: dict[str, ImportProfile] = {
    "visa_credit_card": ImportProfile(
        name="visa_credit_card",
        source_type="visa_credit_card",
        filename_patterns=("visa", "kreditkarte", "credit", "card"),
        delimiter=",",
        encoding="utf-8-sig",
        date_columns=("Date", "ValutaDate"),
        booking_date_columns=("ValutaDate",),
        description_columns=("MerchantName", "Details", "MerchantPlace"),
        amount_columns=("Amount",),
        currency_columns=("Currency",),
        account_columns=("CardId",),
        fingerprint_columns=("TransactionId", "CardId", "Date", "Amount", "Currency", "MerchantName"),
        default_rules=("migros_covered_by_migros", "subscriptions_review", "galaxus_review"),
    ),
    "migros_receipts": ImportProfile(
        name="migros_receipts",
        source_type="migros_receipts",
        filename_patterns=("migros", "cumulus"),
        delimiter=";",
        encoding="utf-8-sig",
        date_columns=("Datum",),
        booking_date_columns=("Datum", "Zeit"),
        description_columns=("Filiale", "Artikel"),
        amount_columns=("Umsatz",),
        currency_columns=(),
        account_columns=("Filiale",),
        fingerprint_columns=("Datum", "Zeit", "Filiale", "Kassennummer", "Transaktionsnummer"),
        default_rules=("receipt_level_candidate", "article_rows_detail_only", "food_household_default"),
    ),
    "raiffeisen_bank": ImportProfile(
        name="raiffeisen_bank",
        source_type="raiffeisen_bank",
        filename_patterns=("raiffeisen",),
        delimiter=";",
        encoding="cp1252",
        date_columns=("Booked At", "Valuta Date"),
        booking_date_columns=("Valuta Date",),
        description_columns=("Text",),
        amount_columns=("Credit/Debit Amount",),
        currency_columns=(),
        account_columns=("IBAN",),
        fingerprint_columns=("IBAN", "Booked At", "Text", "Credit/Debit Amount", "Valuta Date"),
        default_rules=("income_detection", "internal_transfer_detection", "true_wealth_investment_transfer", "credit_card_payment"),
    ),
    "akb_bank": ImportProfile(
        name="akb_bank",
        source_type="akb_bank",
        filename_patterns=("akb", "aargauische", "kantonalbank"),
        delimiter=";",
        encoding="utf-8-sig",
        date_columns=("Buchung", "Valuta"),
        booking_date_columns=("Valuta",),
        description_columns=("Buchungstext",),
        amount_columns=("Belastung", "Gutschrift"),
        currency_columns=(),
        account_columns=(),
        fingerprint_columns=("Buchung", "Valuta", "Buchungstext", "Belastung", "Gutschrift"),
        default_rules=("income_detection", "internal_transfer_detection", "credit_card_payment"),
    ),
}


def _norm(value: object) -> str:
    return str(value or "").strip().lower().replace("\ufeff", "")


def detect_import_profile(columns: list[str]) -> str | None:
    normalized = {_norm(c) for c in columns}
    if {"transactionid", "cardid", "date", "amount", "merchantname"}.issubset(normalized):
        return "visa_credit_card"
    if {"datum", "zeit", "filiale", "transaktionsnummer", "artikel", "umsatz"}.issubset(normalized):
        return "migros_receipts"
    if {"iban", "booked at", "text", "credit/debit amount"}.issubset(normalized):
        return "raiffeisen_bank"
    if {"buchung", "valuta", "buchungstext", "belastung", "gutschrift"}.issubset(normalized):
        return "akb_bank"
    return None


def parse_budget_csv_text(csv_text: str, profile_name: str | None = None) -> dict[str, Any]:
    sample = csv_text[:8192]
    delimiter = IMPORT_PROFILES.get(profile_name or "", ImportProfile("", "", (), ";", "utf-8", (), (), (), (), (), (), (), ())).delimiter
    header = csv_text.splitlines()[0] if csv_text.splitlines() else ""
    raiffeisen_header_detected = False
    if profile_name in {None, "raiffeisen_bank"}:
        for candidate in (",", ";", "\t", "|"):
            header_columns = header.split(candidate)
            normalized_header = {_norm(column) for column in header_columns}
            if (
                {"booked at", "text", "credit/debit amount"}.issubset(normalized_header)
                and normalized_header.intersection({"iban", "account"})
            ):
                delimiter = candidate
                raiffeisen_header_detected = True
                break
    if profile_name == "raiffeisen_bank" and not raiffeisen_header_detected:
        raise ValueError("unsupported Raiffeisen CSV header")
    if not raiffeisen_header_detected:
        try:
            delimiter = csv.Sniffer().sniff(sample, delimiters=";,\t|").delimiter
        except csv.Error:
            pass
    raw_records = list(csv.reader(StringIO(csv_text), delimiter=delimiter))
    if not raw_records:
        raise ValueError("empty budget CSV")
    columns = raw_records[0]
    header_profile = "raiffeisen_bank" if raiffeisen_header_detected else detect_import_profile(list(columns))
    if profile_name is not None and header_profile != profile_name:
        raise ValueError(f"unsupported {profile_name} CSV header")
    detected = profile_name or header_profile
    if detected is None:
        raise ValueError("unsupported budget CSV profile")
    physical_records = raw_records[1:]
    physical_row_count = len(physical_records)
    if detected == "raiffeisen_bank":
        rows = parse_raiffeisen_physical_records(
            physical_records,
            has_balance=any(_norm(column) == "balance" for column in columns),
        )
    else:
        reader = csv.DictReader(StringIO(csv_text), delimiter=delimiter)
        rows = [dict(r) for r in reader]
    return {
        "profile": detected,
        "delimiter": delimiter,
        "columns": columns,
        "rows": rows,
        "physical_row_count": physical_row_count,
        "logical_row_count": len(rows),
    }


def import_profile_specs() -> list[dict[str, Any]]:
    return [p.__dict__ for p in IMPORT_PROFILES.values()]


def _existing_fingerprints(conn: Connection) -> set[str]:
    return {str(r["raw_fingerprint"]) for r in conn.execute("SELECT raw_fingerprint FROM budget_transaction_candidates WHERE raw_fingerprint IS NOT NULL").fetchall()}


def _result(profile: str, dry_run: bool, parsed_rows: int, seed_result: dict[str, Any] | None = None, already: int = 0) -> dict[str, Any]:
    base = {"profile": profile, "dry_run": dry_run, "parsed_rows": parsed_rows, "already_processed_count": already}
    if seed_result:
        base.update(seed_result)
    base.setdefault("candidate_count", 0)
    base.setdefault("possible_duplicate_count", 0)
    base.setdefault("transfer_candidate_count", 0)
    base.setdefault("income_candidate_count", 0)
    base.setdefault("line_item_count", 0)
    return base


def import_budget_csv_text(
    conn: Connection,
    profile_name: str,
    csv_text: str,
    *,
    source_file_label: str,
    dry_run: bool = True,
    default_category_id: str | None = None,
    income_category_id: str | None = None,
) -> dict[str, Any]:
    parsed = parse_budget_csv_text(csv_text, profile_name)
    profile = IMPORT_PROFILES[parsed["profile"]]
    rows = parsed["rows"]
    if dry_run:
        return _result(profile.name, True, len(rows), {"would_create_candidate_count": len(rows)})

    before = _existing_fingerprints(conn)
    if profile.name == "visa_credit_card":
        seed = seed_credit_card_candidates_from_rows(conn, rows, source_file_label=source_file_label, subscription_category_id=default_category_id, migros_covered=True, candidate_source_type=profile.source_type)
    elif profile.name == "migros_receipts":
        seed = seed_migros_candidates_from_rows(conn, rows, source_file_label=source_file_label, auto_category_id=default_category_id, candidate_source_type=profile.source_type)
    elif profile.name in {"raiffeisen_bank", "akb_bank"}:
        seed = seed_bank_transfer_candidates_from_rows(conn, rows, source_file_label=source_file_label, income_category_id=income_category_id, candidate_source_type=profile.source_type)
    else:  # pragma: no cover - guarded by profile map
        raise ValueError(f"unsupported profile {profile.name}")

    duplicate_result = detect_duplicate_candidates(conn)
    after = _existing_fingerprints(conn)
    already = max(0, len(before) + len(rows) - len(after))
    if duplicate_result.get("duplicate_count"):
        seed["possible_duplicate_count"] = max(int(seed.get("possible_duplicate_count") or 0), int(duplicate_result["duplicate_count"]))
    return _result(profile.name, False, len(rows), seed, already=already)
