from __future__ import annotations

from datetime import UTC, datetime
from decimal import Decimal, InvalidOperation
import json
from sqlite3 import Connection
from uuid import uuid4

from fastapi import HTTPException

from jarvis_finance.api.schemas.positions import (
    AccountValueConfirmRequest,
    AccountValuePreviewRequest,
    ActionItem,
    AuditEntry,
    CashConfirmRequest,
    CashPositionDetail,
    CashPreviewRequest,
    CleanupPreviewResponse,
    ConfirmResponse,
    ContainerConfirmRequest,
    ContainerPreviewRequest,
    CryptoActionConfirmRequest,
    CryptoActionPreviewRequest,
    EquityDividendConfirmRequest,
    EquityDividendPreviewRequest,
    EquitySellConfirmRequest,
    EquitySellPreviewRequest,
    GenericConfirmRequest,
    InstrumentSearchCandidate,
    InstrumentSearchRequest,
    PositionConfirmRequest,
    PositionPreviewRequest,
    PreviewResponse,
    PricePoint,
    ProviderSearchResponse,
    ReferenceAccount,
    RemovePositionPreviewResponse,
)
from jarvis_finance.equity.cleanup import apply_review_test_equity_cleanup, identify_review_test_equity_artifacts
from jarvis_finance.fx.rates import resolve_fx_rate_to_chf
from jarvis_finance.imports.common import stable_id
from jarvis_finance.market_data.catalog import (
    CatalogSearchResult,
    FmpSearchProvider,
    FinnhubSymbolLookupProvider,
    MassiveLookupProvider,
    OpenFigiLookupProvider,
    TwelveDataLookupProvider,
    search_instruments as search_catalog_instruments,
)
from jarvis_finance.market_data.cache import get_crypto_chart_points, get_equity_chart_points
from jarvis_finance.services.api_helpers import decimal_text, optional_decimal_text
from jarvis_finance.services.cash_service import get_cash_summary
from jarvis_finance.services.equity_service import get_equity_position


def _now() -> str:
    return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _uuid(prefix: str) -> str:
    return f"{prefix}_{uuid4().hex[:16]}"


def _decimal(value: str, field: str) -> Decimal:
    try:
        parsed = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise HTTPException(status_code=422, detail=f"Invalid decimal for {field}") from exc
    if parsed <= 0:
        raise HTTPException(status_code=422, detail=f"{field} must be positive")
    return parsed


def _decimal_non_negative(value: str | None, field: str) -> Decimal:
    if value in (None, ""):
        return Decimal("0")
    try:
        parsed = Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise HTTPException(status_code=422, detail=f"Invalid decimal for {field}") from exc
    if parsed < 0:
        raise HTTPException(status_code=422, detail=f"{field} must be zero or positive")
    return parsed


def _normalise_transaction_type(value: str, asset_class: str | None = None) -> str:
    key = value.strip().lower().replace(" ", "_").replace("-", "_")
    if key in {"initial_snapshot", "initial"}:
        return "initial_cash_snapshot" if asset_class == "cash" else "initial_position_snapshot"
    mapping = {
        "kauf": "buy",
        "buy": "buy",
        "cash_einzahlung": "cash_deposit",
        "cash_deposit": "cash_deposit",
        "einzahlung": "cash_deposit",
        "cash_auszahlung": "cash_withdrawal",
        "cash_withdrawal": "cash_withdrawal",
        "auszahlung": "cash_withdrawal",
        "korrektur": "manual_cash_correction",
        "cash_korrektur": "manual_cash_correction",
        "manual_cash_correction": "manual_cash_correction",
    }
    return mapping.get(key, key)


def _audit(conn: Connection, *, action: str, entity_type: str, entity_id: str, payload: dict, note: str, quality_status: str = "ok") -> str:
    audit_id = _uuid("audit")
    ts = _now()
    conn.execute(
        """
        INSERT INTO audit_log(audit_id, timestamp, source, action, entity_type, entity_id, old_values_json, new_values_json, user_text_note, confirmed, confirmation_timestamp, created_by, quality_status, created_at)
        VALUES (?, ?, 'vue_dashboard', ?, ?, ?, NULL, ?, ?, 1, ?, 'user', ?, ?)
        """,
        (audit_id, ts, action, entity_type, entity_id, json.dumps(payload, sort_keys=True), note, ts, quality_status, ts),
    )
    return audit_id


def _is_truewealth_account_row(row) -> bool:
    label = f"{row['platform']} {row['account_name']}".lower()
    account_type = str(row["account_type"] or "").lower()
    return account_type in {"robo_portfolio", "managed_portfolio"} or ("true" in label and "wealth" in label and any(token in label for token in {"gesamtwert", "total", "managed"}))


def _account_portfolio_meta(row) -> tuple[str, str, bool]:
    account_type = str(row["account_type"] or "").lower()
    if _is_truewealth_account_row(row):
        return "manual_total_value", "truewealth", False
    if account_type == "brokerage":
        return "positions", "postfinance_equity" if "postfinance" in f"{row['platform']} {row['account_name']}".lower() else "equity", True
    if account_type == "cash":
        return "cash_balance", "cash", False
    return "positions", "other", True


def _is_manual_total_value_account(conn: Connection, account_id: str) -> bool:
    row = conn.execute(
        """
        SELECT a.account_id, p.name AS platform, a.account_name, a.account_type, a.currency
        FROM accounts a JOIN platforms p ON p.platform_id = a.platform_id
        WHERE a.account_id=?
        """,
        (account_id,),
    ).fetchone()
    if not row:
        return False
    valuation_mode, _bucket, allow_position_adds = _account_portfolio_meta(row)
    return valuation_mode == "manual_total_value" and not allow_position_adds


def list_reference_accounts(conn: Connection) -> list[ReferenceAccount]:
    rows = conn.execute(
        """
        SELECT a.account_id, p.name AS platform, a.account_name, a.account_type, a.currency
        FROM accounts a JOIN platforms p ON p.platform_id = a.platform_id
        WHERE a.is_active = 1
        ORDER BY p.name, a.account_name
        """
    ).fetchall()
    accounts: list[ReferenceAccount] = []
    for r in rows:
        valuation_mode, bucket, allow_position_adds = _account_portfolio_meta(r)
        account_type = "managed_portfolio" if bucket == "truewealth" else r["account_type"]
        accounts.append(ReferenceAccount(account_id=r["account_id"], label=f"{r['platform']} · {r['account_name']}", account_type=account_type, currency=r["currency"], valuation_mode=valuation_mode, portfolio_bucket=bucket, allow_position_adds=allow_position_adds))
    return accounts


def search_instruments(conn: Connection, request: InstrumentSearchRequest) -> list[InstrumentSearchCandidate]:
    q = f"%{request.query.strip().lower()}%"
    asset = request.asset_class.lower()
    allowed = ("stock", "etf") if asset in {"stock", "equity", "etf"} else (asset,)
    rows = conn.execute(
        f"""
        SELECT instrument_id, asset_class, name, ticker, isin, exchange, currency
        FROM instruments
        WHERE is_active = 1 AND lower(asset_class) IN ({','.join('?' for _ in allowed)})
          AND (lower(name) LIKE ? OR lower(ticker) LIKE ? OR lower(isin) LIKE ?)
        ORDER BY name
        LIMIT 12
        """,
        (*allowed, q, q, q),
    ).fetchall()
    return [
        InstrumentSearchCandidate(
            candidate_id=r["instrument_id"],
            label=r["name"],
            asset_class=r["asset_class"],
            isin=r["isin"] or "",
            ticker=r["ticker"] or "",
            exchange=r["exchange"] or "",
            currency=r["currency"] or "",
            provider="local",
            confidence_label="Lokal gespeichert",
            review_required=False,
        )
        for r in rows
    ]


def _chf_amount(amount: str | None, rate: Decimal | None) -> str | None:
    if amount in (None, "") or rate is None:
        return None
    return decimal_text(Decimal(str(amount)) * rate, 2)


def _fx_for_explicit_save(conn: Connection, *, currency: str, trade_date: str | None) -> tuple[str, str | None, str | None, str | None]:
    cur = currency.upper()
    result = resolve_fx_rate_to_chf(conn, base_currency=cur, rate_date=trade_date, persist=True, resolve_fixed=True)
    if result.rate is None:
        return "missing", None, None, result.warning or "FX fehlt: Speichern bleibt möglich, CHF-Wert bleibt leer."
    status = "not_needed" if cur == "CHF" else "ok"
    source = result.source
    return status, format(result.rate, "f"), source, None


def _provider_search_providers():
    return [FmpSearchProvider(), FinnhubSymbolLookupProvider(), TwelveDataLookupProvider(), OpenFigiLookupProvider(), MassiveLookupProvider()]


_DERIVATIVE_TOKENS = ("CDR", "ADR", "GDR", "WARRANT", "CERTIFICATE", "DERIVATIVE", "RIGHT", "NOTE")
_MAIN_EXCHANGES = {"NASDAQ", "NYSE", "NYSEARCA", "SIX", "XETRA", "LSE", "TSX"}


def _is_derivative_like(result: CatalogSearchResult) -> bool:
    text = " ".join([result.name or "", result.security_type or "", result.evidence_note or ""]).upper()
    return any(token in text for token in _DERIVATIVE_TOKENS)


def _confidence_label(result: CatalogSearchResult) -> str:
    if "provider conflict" in (result.evidence_note or "").lower():
        return "Manuell prüfen"
    if result.confidence == "high":
        return "Eindeutig"
    if result.confidence == "medium":
        return "Wahrscheinlich"
    if result.ticker or result.isin:
        return "Unsicher"
    return "Manuell prüfen"


def _search_rank(result: CatalogSearchResult, request: InstrumentSearchRequest) -> tuple[int, int, str]:
    q = request.query.strip().upper()
    currency = (request.currency or "").upper()
    exchange = (request.exchange or "").upper()
    result_currency = (result.trading_currency or result.instrument_currency or "").upper()
    result_exchange = (result.exchange or result.provider_market or "").upper()
    rank = 100
    if result.isin and result.isin.upper() == q:
        rank = 0
    elif result.ticker and result.ticker.upper() == q and (not exchange or result_exchange == exchange) and (not currency or result_currency == currency):
        rank = 1
    elif result.ticker and result.ticker.upper() == q:
        rank = 2
    elif normalize_text(result.name) == normalize_text(request.query):
        rank = 3
    elif q in normalize_text(result.name).upper().split():
        rank = 4
    else:
        rank = 8
    penalty = 0
    if result_exchange in _MAIN_EXCHANGES:
        penalty -= 2
    if _is_derivative_like(result):
        penalty += 20
    if result.confidence == "low":
        penalty += 5
    return (rank, penalty, result.name.lower())


def normalize_text(value: str | None) -> str:
    return " ".join(str(value or "").replace("-", " ").split()).lower()


def _provider_warning_label(code: str) -> str:
    lowered = code.lower()
    if "api_key_missing" in lowered:
        return "API-Key fehlt"
    if "rate_limited" in lowered:
        return "Rate Limit"
    if "network" in lowered:
        return "Provider nicht erreichbar"
    if "endpoint_restricted" in lowered:
        return "Endpoint eingeschränkt"
    if "auth_failed" in lowered:
        return "API-Key ungültig oder Provider nicht autorisiert"
    if "provider_error" in lowered or "lookup_unavailable" in lowered:
        return "Backend-/Provider-Fehler"
    return code


def _user_provider_source_hint(candidates: list[InstrumentSearchCandidate], warnings: list[str]) -> str | None:
    if not candidates or not warnings:
        return None
    providers = sorted({p.strip() for c in candidates for p in (c.provider or "").replace("+", ",").split(",") if p.strip()})
    source = "/".join(providers[:3]) if providers else "einem anderen Provider"
    return f"Datenquelle teilweise eingeschränkt: Ein Provider lieferte keine Daten; Treffer stammt aus {source}."


def _plausible_currency(result: CatalogSearchResult, request: InstrumentSearchRequest) -> str:
    explicit = (result.trading_currency or result.instrument_currency or "").upper()
    if explicit:
        return explicit
    exchange = (result.exchange or result.provider_market or request.exchange or "").upper()
    if exchange in {"NASDAQ", "NYSE", "NYSEARCA", "AMEX", "ARCA", "XNAS", "XNYS"}:
        return "USD"
    if exchange in {"SIX", "XSWX"}:
        return "CHF"
    if exchange in {"XETRA", "FWB", "XFRA"}:
        return "EUR"
    return (request.currency or "").upper()


def _passes_provider_filters(result: CatalogSearchResult, request: InstrumentSearchRequest) -> bool:
    if request.hide_cdr_adr_derivatives and _is_derivative_like(result):
        return False
    if request.country and request.country.upper() not in {"ALL", "ALLE"} and (result.country or "").upper() != request.country.upper():
        return False
    if request.currency and request.currency.upper() not in {"ALL", "ALLE"}:
        if (result.trading_currency or result.instrument_currency or "").upper() != request.currency.upper():
            return False
    if request.exchange and request.exchange.upper() not in {"ALL", "ALLE"}:
        if (result.exchange or result.provider_market or "").upper() != request.exchange.upper():
            return False
    if request.main_listing_only:
        ex = (result.exchange or result.provider_market or "").upper()
        if ex and ex not in _MAIN_EXCHANGES:
            return False
    return True


def _ensure_instrument_from_request(conn: Connection, request: PositionPreviewRequest) -> str | None:
    instrument_id = request.instrument_id or None
    if instrument_id and conn.execute("SELECT instrument_id FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone():
        return instrument_id
    candidate_id = request.candidate_id or None
    if candidate_id and conn.execute("SELECT instrument_id FROM instruments WHERE instrument_id=?", (candidate_id,)).fetchone():
        return candidate_id
    catalog = conn.execute("SELECT * FROM instrument_catalog_entries WHERE catalog_entry_id=?", (candidate_id,)).fetchone() if candidate_id else None
    if catalog:
        existing = None
        if catalog["isin"]:
            currency = (catalog["instrument_currency"] or catalog["trading_currency"] or request.currency.upper())
            existing = conn.execute("SELECT instrument_id FROM instruments WHERE isin=? AND COALESCE(exchange,'')=COALESCE(?, '') AND COALESCE(currency,'')=COALESCE(?, '')", (catalog["isin"], catalog["exchange"], currency)).fetchone()
        if existing:
            return existing["instrument_id"]
        instrument_id = stable_id("instrument", catalog["isin"] or "", catalog["ticker"] or "", catalog["exchange"] or "", catalog["name"])
        ts = _now()
        conn.execute(
            """
            INSERT OR IGNORE INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, country, provider_symbol, data_provider_primary, trading_currency, instrument_status, valuation_policy, notes, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unknown', 'live_price', ?, ?, ?)
            """,
            (instrument_id, catalog["asset_class"], catalog["name"], catalog["ticker"], catalog["isin"], catalog["exchange"], catalog["instrument_currency"] or catalog["trading_currency"] or request.currency.upper(), catalog["country"], catalog["provider_symbol"], catalog["provider"] or catalog["source"], catalog["trading_currency"] or request.currency.upper(), "Materialized from explicit Vue provider-search selection; no provider call during preview/confirm.", ts, ts),
        )
        return instrument_id
    if request.name or request.ticker or request.isin:
        instrument_id = stable_id("instrument", request.isin or "", request.ticker or "", request.name or "", request.currency.upper())
        ts = _now()
        conn.execute(
            """
            INSERT OR IGNORE INTO instruments(instrument_id, asset_class, name, ticker, isin, exchange, currency, trading_currency, instrument_status, valuation_policy, notes, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, '', ?, ?, 'unknown', 'live_price', ?, ?, ?)
            """,
            (instrument_id, request.asset_class.lower(), request.name or request.ticker or request.isin or "Manuelles Instrument", request.ticker, request.isin, request.currency.upper(), request.currency.upper(), "Materialized from manual Vue input; explicit review required.", ts, ts),
        )
        return instrument_id
    return None


def _instrument_preview_row(conn: Connection, request: PositionPreviewRequest):
    instrument_id = request.instrument_id or None
    if instrument_id:
        inst = conn.execute("SELECT name, currency, asset_class, ticker, isin, exchange FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
        if inst:
            return inst
    candidate_id = request.candidate_id or None
    if candidate_id:
        inst = conn.execute("SELECT name, currency, asset_class, ticker, isin, exchange FROM instruments WHERE instrument_id=?", (candidate_id,)).fetchone()
        if inst:
            return inst
        catalog = conn.execute("SELECT name, COALESCE(NULLIF(instrument_currency,''), NULLIF(trading_currency,'')) AS currency, asset_class, ticker, isin, exchange FROM instrument_catalog_entries WHERE catalog_entry_id=?", (candidate_id,)).fetchone()
        if catalog:
            return catalog
    if request.name or request.ticker or request.isin:
        return {
            "name": request.name or request.ticker or request.isin or "Manuelles Instrument",
            "currency": request.currency.upper(),
            "asset_class": request.asset_class.lower(),
            "ticker": request.ticker,
            "isin": request.isin,
            "exchange": "",
        }
    return None


def _dedupe_provider_results(results: list[CatalogSearchResult]) -> list[CatalogSearchResult]:
    merged: dict[tuple[str, ...], CatalogSearchResult] = {}
    for result in results:
        key = ("isin", result.isin.upper(), (result.exchange or result.provider_market or "").upper(), (result.trading_currency or result.instrument_currency or "").upper()) if result.isin else (("provider", (result.provider or result.source or "").lower(), (result.provider_symbol or result.ticker or "").upper(), (result.exchange or result.provider_market or "").upper(), (result.trading_currency or result.instrument_currency or "").upper()))
        existing = merged.get(key)
        if existing is None:
            merged[key] = result
            continue
        providers = sorted({p for p in [existing.provider, result.provider] if p})
        sources = sorted({p for p in [existing.source, result.source] if p})
        existing.provider = "+".join(providers) if providers else existing.provider
        existing.source = "+".join(sources) if sources else existing.source
        if _confidence_label(existing) != "Manuell prüfen" and _confidence_label(result) != "Manuell prüfen":
            existing.confidence = "high" if existing.confidence in {"medium", "high"} or result.confidence in {"medium", "high"} else existing.confidence
        existing.exchange_name = existing.exchange_name or result.exchange_name
        existing.mic = existing.mic or result.mic
        existing.security_type = existing.security_type or result.security_type
        existing.last_price = existing.last_price or result.last_price
        existing.price_currency = existing.price_currency or result.price_currency
        existing.price_date = existing.price_date or result.price_date
        existing.country = existing.country or result.country
    return list(merged.values())

def provider_search_instruments(conn: Connection, request: InstrumentSearchRequest) -> ProviderSearchResponse:
    if not request.query.strip():
        raise HTTPException(status_code=422, detail="Search query required")
    results, warnings = search_catalog_instruments(conn, request.query, request.asset_class, providers=_provider_search_providers())
    filtered = _dedupe_provider_results([r for r in results if _passes_provider_filters(r, request)])
    filtered.sort(key=lambda r: _search_rank(r, request))
    candidates = [
        InstrumentSearchCandidate(
            candidate_id=r.catalog_entry_id or stable_id("candidate", r.source, r.name, r.isin or "", r.ticker or "", r.exchange or ""),
            label=r.name,
            asset_class=r.asset_class,
            isin=r.isin or "",
            ticker=r.ticker or "",
            exchange=r.exchange or r.provider_market or "",
            currency=_plausible_currency(r, request),
            provider=r.provider or r.source,
            provider_symbol=r.provider_symbol or r.ticker or "",
            exchange_name=r.exchange_name or "",
            mic=r.mic or r.provider_market or "",
            country=r.country or "",
            security_type=r.security_type or "",
            last_price=r.last_price,
            price_currency=r.price_currency,
            price_date=r.price_date,
            confidence_label=_confidence_label(r),
            review_required=True,
        )
        for r in filtered[:12]
    ]
    ui_warnings: list[str] = []
    if candidates:
        hint = _user_provider_source_hint(candidates, warnings)
        if hint:
            ui_warnings.append(hint)
    else:
        ui_warnings.append("Keine verlässlichen Providerdaten gefunden.")
    if len(candidates) > 1:
        ui_warnings.append("Mehrdeutige Treffer")
    return ProviderSearchResponse(candidates=candidates, warnings=sorted(set(ui_warnings)), selection_required=True)


def _cache_candidate_price(conn: Connection, *, instrument_id: str, candidate_id: str | None) -> None:
    if not candidate_id:
        return
    catalog = conn.execute("SELECT last_price, price_currency, trading_currency, instrument_currency, provider, source FROM instrument_catalog_entries WHERE catalog_entry_id=?", (candidate_id,)).fetchone()
    if not catalog or not catalog["last_price"]:
        return
    currency = (catalog["price_currency"] or catalog["trading_currency"] or catalog["instrument_currency"] or "").upper()
    if not currency:
        return
    price_date = _now()[:10]
    price_id = stable_id("mprice", instrument_id, price_date, catalog["last_price"], currency, "candidate")
    conn.execute(
        """
        INSERT OR IGNORE INTO market_prices(market_price_id, instrument_id, price_date, close, currency, provider, quality_status, created_at)
        VALUES (?, ?, ?, ?, ?, ?, 'ok', ?)
        """,
        (price_id, instrument_id, price_date, str(catalog["last_price"]), currency, catalog["provider"] or catalog["source"] or "provider_search", _now()),
    )


def preview_cash(conn: Connection, request: CashPreviewRequest) -> PreviewResponse:
    _decimal(request.amount, "amount")
    account = conn.execute("SELECT account_id FROM accounts WHERE account_id=?", (request.account_id,)).fetchone()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    fx_status = "not_needed" if request.currency.upper() == "CHF" else "missing"
    warnings = [] if fx_status == "not_needed" else ["FX fehlt: Speichern bleibt möglich, CHF-Wert wird leer markiert."]
    amount_chf = decimal_text(request.amount, 2) if fx_status == "not_needed" else None
    return PreviewResponse(preview_id=_uuid("preview"), summary=f"{request.transaction_type} {request.currency.upper()} {request.amount}", amount_chf=amount_chf, fx_status=fx_status, warnings=warnings)


def confirm_cash(conn: Connection, request: CashConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview = preview_cash(conn, request)
    balance_id = _uuid("tx")
    ts = _now()
    fx_rate = "1" if request.currency.upper() == "CHF" else None
    quality = "ok" if request.currency.upper() == "CHF" else "missing_fx"
    amount = str(_decimal(request.amount, "amount"))
    transaction_type = _normalise_transaction_type(request.transaction_type, "cash")
    conn.execute(
        """
        INSERT INTO transactions(transaction_id, transaction_type, account_id, trade_date, gross_amount_original, net_amount_original, currency_original, fx_rate_to_chf, fx_status, gross_amount_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_manual_cash', 1, ?, ?, ?)
        """,
        (balance_id, transaction_type, request.account_id, request.booking_date, amount, amount, request.currency.upper(), fx_rate, "ok" if fx_rate else "missing", preview.amount_chf, preview.amount_chf, quality, request.note, ts),
    )
    conn.execute(
        """
        INSERT OR IGNORE INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, 'vue_manual_cash', ?, ?, ?)
        """,
        (_uuid("cash"), request.account_id, request.booking_date, request.currency.upper(), amount, fx_rate, preview.amount_chf, quality, request.note, ts),
    )
    audit_id = _audit(conn, action="cash_confirm", entity_type="cash_position", entity_id=f"{request.account_id}:{request.currency.upper()}", payload=request.model_dump(), note=request.note, quality_status=quality)
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=f"{request.account_id}:{request.currency.upper()}", audit_id=audit_id, message="Cash gespeichert")


def preview_position(conn: Connection, request: PositionPreviewRequest) -> PreviewResponse:
    _decimal(request.quantity, "quantity")
    account = conn.execute("SELECT account_id FROM accounts WHERE account_id=?", (request.account_id,)).fetchone()
    if not account:
        raise HTTPException(status_code=404, detail="Account not found")
    if _is_manual_total_value_account(conn, request.account_id):
        raise HTTPException(status_code=422, detail="TrueWealth/managed portfolio is a manually valued Gesamtwert-Konto; Einzelpositionen sind hier nicht erlaubt.")
    inst = _instrument_preview_row(conn, request)
    if not inst:
        raise HTTPException(status_code=404, detail="Instrument not found")
    currency = request.currency.upper()
    fx_status, fx_rate, _fx_source, fx_warning = _fx_for_explicit_save(conn, currency=currency, trade_date=request.trade_date)
    warnings = [fx_warning] if fx_warning else []
    amount_chf = _chf_amount(request.cost_basis_original, Decimal(fx_rate) if fx_rate else None)
    return PreviewResponse(preview_id=_uuid("preview"), asset_class=request.asset_class.lower(), summary=f"{inst['name']} · {request.quantity} · {currency}", amount_chf=amount_chf, fx_status=fx_status, warnings=warnings)


def confirm_position(conn: Connection, request: PositionConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview_position(conn, request)
    instrument_id = _ensure_instrument_from_request(conn, request)
    if instrument_id is None:
        raise HTTPException(status_code=404, detail="Instrument not found")
    _cache_candidate_price(conn, instrument_id=instrument_id, candidate_id=request.candidate_id)
    tx_id = _uuid("tx")
    ts = _now()
    cost = request.cost_basis_original or "0"
    fx_status, fx_rate, fx_source, _fx_warning = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.trade_date)
    gross_chf = _chf_amount(cost, Decimal(fx_rate) if fx_rate else None)
    quality_parts = []
    if not request.cost_basis_original:
        quality_parts.append("cost_basis_missing")
    if fx_status == "missing":
        quality_parts.append("missing_fx")
    quality = ";".join(quality_parts) or "ok"
    transaction_type = _normalise_transaction_type(request.transaction_type)
    conn.execute(
        """
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, gross_amount_original, net_amount_original, currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_manual_position', 1, ?, ?, ?)
        """,
        (tx_id, transaction_type, request.account_id, instrument_id, request.trade_date, str(_decimal(request.quantity, "quantity")), cost, cost, request.currency.upper(), fx_rate, fx_source, fx_status, gross_chf, gross_chf, quality, request.note, ts),
    )
    audit_id = _audit(conn, action="position_confirm", entity_type="position", entity_id=f"{request.account_id}:{instrument_id}", payload=request.model_dump(), note=request.note, quality_status=quality)
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=tx_id, audit_id=audit_id, message="Position gespeichert")


def get_cash_position_detail(conn: Connection, position_id: str) -> CashPositionDetail:
    position = next((p for p in get_cash_summary(conn).positions if p.id == position_id), None)
    account_id, currency = position_id.split(":", 1)
    if not position:
        row = conn.execute(
            """
            SELECT p.name AS platform, a.account_name, cb.currency, cb.amount_original, cb.amount_chf, cb.quality_status, MAX(cb.created_at) AS last_change
            FROM cash_balances cb JOIN accounts a ON a.account_id = cb.account_id JOIN platforms p ON p.platform_id = a.platform_id
            WHERE cb.account_id=? AND cb.currency=?
            GROUP BY cb.account_id, cb.currency
            """,
            (account_id, currency),
        ).fetchone()
        if not row:
            raise HTTPException(status_code=404, detail="Cash position not found")
        payload = {"id": position_id, "platform": row["platform"], "account_label": row["account_name"], "currency": row["currency"], "amount": optional_decimal_text(row["amount_original"]) or "0", "amount_chf": optional_decimal_text(row["amount_chf"], 2), "status": row["quality_status"] or "ok"}
        last_change = row["last_change"]
    else:
        payload = position.model_dump()
        row = conn.execute("SELECT MAX(created_at) AS last_change FROM cash_balances WHERE account_id=? AND currency=?", (account_id, currency)).fetchone()
        last_change = row["last_change"] if row else None
    return CashPositionDetail(**payload, last_change=last_change, available_actions=[ActionItem(label="Einzahlung", enabled=True), ActionItem(label="Auszahlung", enabled=True), ActionItem(label="Korrektur", enabled=True), ActionItem(label="Verlauf", enabled=True)])


def get_equity_position_detail(conn: Connection, position_id: str):
    account_id, instrument_id = position_id.split(":", 1)
    try:
        base = get_equity_position(conn, position_id)
        payload = base.model_dump()
    except HTTPException:
        row = conn.execute(
            """
            SELECT i.name, i.ticker, i.isin, i.asset_class, i.currency, a.account_name
            FROM instruments i JOIN accounts a ON a.account_id = ?
            WHERE i.instrument_id = ?
            """,
            (account_id, instrument_id),
        ).fetchone()
        if not row:
            raise
        payload = {
            "position_id": position_id,
            "name": row["name"],
            "ticker": row["ticker"] or "",
            "isin": row["isin"] or "",
            "account": row["account_name"] or "",
            "asset_class": "ETF" if str(row["asset_class"]).lower() == "etf" else "Aktie",
            "quantity": "0",
            "currency": row["currency"] or "CHF",
            "price": None,
            "market_value_chf": None,
            "status": "Einstand unvollständig",
        }
    rows = get_equity_chart_points(conn, instrument_id, limit=30)
    tx_fx = conn.execute(
        """
        SELECT fx_status, fx_rate_to_chf, currency_original
        FROM transactions
        WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0
        ORDER BY trade_date DESC, created_at DESC
        LIMIT 1
        """,
        (account_id, instrument_id),
    ).fetchone()
    inst_meta = conn.execute(
        """
        SELECT i.currency, i.exchange, i.provider_symbol, i.data_provider_primary,
               mp.currency AS price_currency, mp.provider AS price_provider,
               mp.provider_symbol AS price_provider_symbol,
               mp.close AS latest_price
        FROM instruments i
        LEFT JOIN market_prices mp ON mp.market_price_id = (
            SELECT market_price_id FROM market_prices
            WHERE instrument_id=i.instrument_id AND quality_status IN ('fresh','ok')
            ORDER BY COALESCE(price_timestamp, created_at, price_date) DESC LIMIT 1
        )
        WHERE i.instrument_id=?
        """,
        (instrument_id,),
    ).fetchone()
    mapping = conn.execute(
        """
        SELECT provider, provider_symbol, provider_market, currency
        FROM instrument_price_mappings
        WHERE instrument_id=? AND mapping_status='mapped' AND provider_symbol IS NOT NULL
        ORDER BY created_at DESC LIMIT 1
        """,
        (instrument_id,),
    ).fetchone()
    inst_currency = inst_meta["currency"] if inst_meta else None
    currency = str((tx_fx["currency_original"] if tx_fx and tx_fx["currency_original"] else None) or inst_currency or payload.get("currency") or "CHF").upper()
    if currency == "CHF":
        fx_status = "not_needed"
    elif tx_fx and tx_fx["fx_status"] == "ok" and tx_fx["fx_rate_to_chf"]:
        fx_status = "ok"
    else:
        fx_status = "missing"
    return {
        **payload,
        "fx_status": fx_status,
        "cost_basis_status": "ok" if payload.get("market_value_chf") else "Einstand unvollständig",
        "day_change_chf": None,
        "last_trade_date": rows[-1]["timestamp"] if rows else None,
        "provider": rows[-1]["provider"] if rows else (inst_meta["price_provider"] if inst_meta else None),
        "source_url": None,
        "provider_symbol": (mapping["provider_symbol"] if mapping else None) or (inst_meta["price_provider_symbol"] if inst_meta else None) or (inst_meta["provider_symbol"] if inst_meta else None),
        "exchange": (mapping["provider_market"] if mapping else None) or (inst_meta["exchange"] if inst_meta else None),
        "price_currency": (inst_meta["price_currency"] if inst_meta else None) or payload.get("currency"),
        "fx_rate_to_chf": tx_fx["fx_rate_to_chf"] if tx_fx else None,
        "data_source": (mapping["provider"] if mapping else None) or (inst_meta["price_provider"] if inst_meta else None) or (inst_meta["data_provider_primary"] if inst_meta else None),
        "available_actions": [a.model_dump() for a in [ActionItem(label="Kaufen", enabled=True), ActionItem(label="Verkaufen", enabled=True), ActionItem(label="Dividende/Ausschüttung", enabled=True), ActionItem(label="Menge korrigieren", enabled=False, reason="nächster Sprint"), ActionItem(label="Verlauf", enabled=True)]],
        "price_history": [PricePoint(date=r["timestamp"], value=optional_decimal_text(r["price"]) or "0", currency=r["currency"] or "CHF", provider=r["provider"], quality_status=r["source_quality"]).model_dump() for r in rows],
    }


def list_audit(conn: Connection, entity_type: str | None = None, entity_id: str | None = None) -> list[AuditEntry]:
    clauses=[]; params=[]
    if entity_type:
        clauses.append("entity_type=?"); params.append(entity_type)
    if entity_id:
        clauses.append("entity_id=?"); params.append(entity_id)
    where = "WHERE " + " AND ".join(clauses) if clauses else ""
    rows = conn.execute(f"SELECT audit_id, timestamp, action, entity_type, entity_id, user_text_note, quality_status FROM audit_log {where} ORDER BY timestamp DESC LIMIT 50", params).fetchall()
    return [AuditEntry(audit_id=r["audit_id"], timestamp=r["timestamp"], action=r["action"], entity_type=r["entity_type"], entity_id=r["entity_id"], note=r["user_text_note"], quality_status=r["quality_status"]) for r in rows]


def market_price_history(conn: Connection, instrument_id: str) -> list[PricePoint]:
    rows = get_equity_chart_points(conn, instrument_id, limit=120)
    return [PricePoint(date=r["timestamp"], value=optional_decimal_text(r["price"]) or "0", currency=r["currency"] or "CHF", provider=r["provider"], quality_status=r["source_quality"]) for r in rows]


def crypto_price_history(conn: Connection, asset_id: str) -> list[PricePoint]:
    rows = get_crypto_chart_points(conn, asset_id, currency="CHF", limit=60)
    return [PricePoint(date=r["timestamp"] or "", value=decimal_text(r["price"]), currency=r["currency"] or "CHF", provider=r["provider"], quality_status=r["source_quality"]) for r in rows]


def _crypto_asset_identity(conn: Connection, request: CryptoActionPreviewRequest) -> tuple[str | None, str, str]:
    if request.asset_id:
        row = conn.execute("SELECT asset_id, coin_name, symbol FROM crypto_assets WHERE asset_id=?", (request.asset_id,)).fetchone()
        if row is None:
            raise HTTPException(status_code=404, detail="Crypto asset not found")
        return str(row["asset_id"]), str(row["coin_name"]), str(row["symbol"])
    coin_name = str(request.coin_name or "").strip()
    symbol = str(request.symbol or "").strip().upper()
    if not coin_name or not symbol:
        raise HTTPException(status_code=422, detail="coin_name and symbol are required for a new crypto asset")
    return None, coin_name, symbol


def _crypto_action_label(transaction_type: str) -> str:
    key = transaction_type.strip().lower().replace(" ", "_").replace("-", "_")
    return {
        "initial_snapshot": "Crypto Initial Snapshot",
        "initial_snapshot_addition": "Crypto Initial Snapshot",
        "increase": "Crypto Bestand erhöhen",
        "bestand_erhoehen": "Crypto Bestand erhöhen",
        "decrease": "Crypto Bestand reduzieren",
        "bestand_reduzieren": "Crypto Bestand reduzieren",
        "correction": "Crypto Bestand korrigieren",
        "manual_adjustment": "Crypto Bestand korrigieren",
        "set_zero": "Crypto auf 0 setzen",
        "auf_0_setzen": "Crypto auf 0 setzen",
        "transfer": "Crypto Transfer",
    }.get(key, "Crypto Anpassung")


def _crypto_operation(transaction_type: str) -> str:
    return transaction_type.strip().lower().replace(" ", "_").replace("-", "_")


def _format_decimal(value: Decimal) -> str:
    return format(value, "f")


def _set_crypto_holding(conn: Connection, *, wallet_id: str, asset_id: str, quantity: Decimal) -> None:
    now = _now()
    existing = conn.execute("SELECT crypto_holding_id FROM crypto_holdings WHERE wallet_id=? AND asset_id=?", (wallet_id, asset_id)).fetchone()
    if existing:
        conn.execute("UPDATE crypto_holdings SET quantity=?, updated_at=? WHERE crypto_holding_id=?", (_format_decimal(quantity), now, existing["crypto_holding_id"]))
    else:
        conn.execute("INSERT INTO crypto_holdings(crypto_holding_id, wallet_id, asset_id, quantity, verification_status, created_at) VALUES (?, ?, ?, ?, 'ok', ?)", (_uuid("holding"), wallet_id, asset_id, _format_decimal(quantity), now))


def _apply_crypto_transfer_holdings(conn: Connection, *, asset_id: str, from_wallet_id: str, to_wallet_id: str, quantity: Decimal, fee_quantity: Decimal) -> None:
    source_row = conn.execute("SELECT quantity FROM crypto_holdings WHERE wallet_id=? AND asset_id=?", (from_wallet_id, asset_id)).fetchone()
    source_before = Decimal(str(source_row["quantity"])) if source_row else Decimal("0")
    source_after = source_before - quantity - fee_quantity
    if source_after < 0:
        raise ValueError("negative wallet balance blocked")
    target_row = conn.execute("SELECT quantity FROM crypto_holdings WHERE wallet_id=? AND asset_id=?", (to_wallet_id, asset_id)).fetchone()
    target_before = Decimal(str(target_row["quantity"])) if target_row else Decimal("0")
    _set_crypto_holding(conn, wallet_id=from_wallet_id, asset_id=asset_id, quantity=source_after)
    _set_crypto_holding(conn, wallet_id=to_wallet_id, asset_id=asset_id, quantity=target_before + quantity)


def preview_crypto_action(conn: Connection, request: CryptoActionPreviewRequest) -> PreviewResponse:
    _, coin_name, symbol = _crypto_asset_identity(conn, request)
    if _crypto_operation(request.transaction_type) in {"set_zero", "auf_0_setzen"}:
        try:
            quantity = Decimal(str(request.quantity))
        except (InvalidOperation, ValueError) as exc:
            raise HTTPException(status_code=422, detail="Invalid decimal for quantity") from exc
        if quantity < 0:
            raise HTTPException(status_code=422, detail="quantity must be zero or positive")
    else:
        quantity = _decimal(request.quantity, "quantity")
    op = _crypto_operation(request.transaction_type)
    label = _crypto_action_label(request.transaction_type)
    wallet = conn.execute("SELECT wallet_name FROM crypto_wallets WHERE wallet_id=?", (request.wallet_id,)).fetchone()
    if wallet is None:
        raise HTTPException(status_code=404, detail="Wallet not found")
    warnings: list[str] = []
    if op == "transfer":
        if not request.target_wallet_id:
            raise HTTPException(status_code=422, detail="target_wallet_id is required for transfer")
        target_wallet = conn.execute("SELECT wallet_name FROM crypto_wallets WHERE wallet_id=?", (request.target_wallet_id,)).fetchone()
        if target_wallet is None:
            raise HTTPException(status_code=404, detail="Target wallet not found")
        if request.target_wallet_id == request.wallet_id:
            raise HTTPException(status_code=422, detail="source and target wallets must be different")
        fee = _decimal_non_negative(request.fee_quantity, "fee_quantity")
        current = conn.execute("SELECT quantity FROM crypto_holdings WHERE wallet_id=? AND asset_id=?", (request.wallet_id, request.asset_id)).fetchone()
        available = Decimal(str(current["quantity"])) if current else Decimal("0")
        if available < quantity + fee:
            raise HTTPException(status_code=422, detail="negative wallet balance blocked")
        return PreviewResponse(preview_id=_uuid("preview"), asset_class="crypto", summary=f"{label}: {quantity} {symbol} von {wallet['wallet_name']} nach {target_wallet['wallet_name']} · Fee {fee}", amount_chf=None, fx_status="not_needed", warnings=warnings)
    if not request.note.strip() and op not in {"initial_snapshot", "initial_snapshot_addition"}:
        warnings.append("Notiz ist für Korrekturen/Bestandsänderungen empfohlen und beim Confirm erforderlich.")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="crypto", summary=f"{label}: {quantity} {symbol} in {wallet['wallet_name']} · {coin_name}", amount_chf=None, fx_status="not_needed", warnings=warnings)


def confirm_crypto_action(conn: Connection, request: CryptoActionConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=422, detail="confirm=true required")
    asset_id, coin_name, symbol = _crypto_asset_identity(conn, request)
    op = _crypto_operation(request.transaction_type)
    try:
        from jarvis_finance.crypto.manage import add_crypto_position, adjust_crypto_position, reduce_crypto_position, transfer_crypto_position
        if op in {"initial_snapshot", "initial_snapshot_addition"}:
            result = add_crypto_position(conn, coin_name=coin_name, symbol=symbol, wallet_id=request.wallet_id, quantity_text=request.quantity, effective_date=request.effective_date, operation_type="initial_snapshot_addition", note=request.note, confirm=True, coingecko_id=request.coingecko_id)
            entity_id = result.holding_id or result.asset_id
        elif op in {"increase", "bestand_erhoehen"}:
            result = add_crypto_position(conn, coin_name=coin_name, symbol=symbol, wallet_id=request.wallet_id, quantity_text=request.quantity, effective_date=request.effective_date, operation_type="manual_adjustment", note=request.note, confirm=True, coingecko_id=request.coingecko_id)
            entity_id = result.transaction_id or result.asset_id
        elif op in {"decrease", "bestand_reduzieren"}:
            if not asset_id:
                raise ValueError("existing asset is required for reduction")
            result = reduce_crypto_position(conn, asset_id=asset_id, wallet_id=request.wallet_id, reduction_quantity_text=request.quantity, effective_date=request.effective_date, note=request.note, confirm=True)
            entity_id = result.transaction_id or result.asset_id
        elif op in {"correction", "manual_adjustment", "set_zero", "auf_0_setzen"}:
            if not asset_id:
                raise ValueError("existing asset is required for correction")
            target_quantity = "0" if op in {"set_zero", "auf_0_setzen"} else request.quantity
            result = adjust_crypto_position(conn, asset_id=asset_id, wallet_id=request.wallet_id, new_quantity_text=target_quantity, effective_date=request.effective_date, note=request.note, confirm=True)
            entity_id = result.transaction_id or result.asset_id
        elif op == "transfer":
            if not asset_id:
                raise ValueError("existing asset is required for transfer")
            if not request.target_wallet_id:
                raise ValueError("target wallet is required for transfer")
            result = transfer_crypto_position(conn, asset_id=asset_id, from_wallet_id=request.wallet_id, to_wallet_id=request.target_wallet_id, quantity_text=request.quantity, fee_quantity_text=request.fee_quantity, effective_date=request.effective_date, note=request.note, confirm=True)
            _apply_crypto_transfer_holdings(conn, asset_id=asset_id, from_wallet_id=request.wallet_id, to_wallet_id=request.target_wallet_id, quantity=_decimal(request.quantity, "quantity"), fee_quantity=_decimal_non_negative(request.fee_quantity, "fee_quantity"))
            entity_id = result.transaction_id or result.asset_id
            audit_id = _audit(conn, action="crypto_transfer_confirm", entity_type="crypto_transaction", entity_id=entity_id, payload=request.model_dump(), note=request.note, quality_status="ok")
            conn.commit()
            return ConfirmResponse(status="confirmed", entity_id=entity_id, audit_id=audit_id, message="Crypto Transfer bestätigt · Audit geschrieben")
        else:
            raise ValueError("unsupported crypto action")
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    audit_row = conn.execute("SELECT audit_id FROM audit_log WHERE entity_id=? ORDER BY created_at DESC LIMIT 1", (entity_id,)).fetchone()
    audit_id = audit_row["audit_id"] if audit_row else _audit(conn, action="crypto_action_confirm", entity_type="crypto_transaction", entity_id=entity_id, payload=request.model_dump(), note=request.note, quality_status="ok")
    return ConfirmResponse(status="confirmed", entity_id=entity_id, audit_id=audit_id, message="Crypto Aktion bestätigt · Audit geschrieben")


def _position_ids(position_id: str) -> tuple[str, str]:
    if ":" not in position_id:
        raise HTTPException(status_code=422, detail="position_id must be account_id:instrument_id")
    return tuple(position_id.split(":", 1))  # type: ignore[return-value]


def _current_equity_quantity(conn: Connection, *, account_id: str, instrument_id: str) -> Decimal:
    row = conn.execute("SELECT SUM(CAST(quantity AS TEXT)) AS quantity FROM transactions WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0 AND quantity IS NOT NULL", (account_id, instrument_id)).fetchone()
    return Decimal(str(row["quantity"] or "0")) if row else Decimal("0")


def _weighted_average_cost(conn: Connection, *, account_id: str, instrument_id: str) -> Decimal | None:
    rows = conn.execute("SELECT quantity, gross_amount_original FROM transactions WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0 AND CAST(quantity AS TEXT) > '0' AND gross_amount_original IS NOT NULL", (account_id, instrument_id)).fetchall()
    total_qty = Decimal("0"); total_cost = Decimal("0")
    for row in rows:
        qty = Decimal(str(row["quantity"] or "0"))
        cost = Decimal(str(row["gross_amount_original"] or "0"))
        if qty > 0:
            total_qty += qty; total_cost += cost
    if total_qty <= 0:
        return None
    return total_cost / total_qty


def _cash_account_exists(conn: Connection, account_id: str | None) -> bool:
    if not account_id:
        return False
    return conn.execute("SELECT account_id FROM accounts WHERE account_id=? AND is_active=1", (account_id,)).fetchone() is not None


def preview_equity_sell(conn: Connection, request: EquitySellPreviewRequest) -> PreviewResponse:
    account_id, instrument_id = _position_ids(request.position_id)
    quantity = _decimal(request.quantity, "quantity")
    price = _decimal(request.price_original, "price_original")
    fees = _decimal_non_negative(request.fees_original, "fees_original")
    current = _current_equity_quantity(conn, account_id=account_id, instrument_id=instrument_id)
    if quantity > current:
        raise HTTPException(status_code=422, detail="Verkauf würde Bestand negativ machen")
    gross = quantity * price
    net = gross - fees
    if net < 0:
        raise HTTPException(status_code=422, detail="fees exceed gross proceeds")
    currency = request.currency.upper()
    fx_status, fx_rate, _fx_source, fx_warning = _fx_for_explicit_save(conn, currency=currency, trade_date=request.trade_date)
    amount_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    avg = _weighted_average_cost(conn, account_id=account_id, instrument_id=instrument_id)
    warnings = [fx_warning] if fx_warning else []
    if avg is None:
        warnings.append("Einstand unvollständig: realisierter P&L nur eingeschränkt verfügbar.")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="equity", summary=f"Verkauf {quantity} · Erlös {currency} {_format_decimal(net)}", amount_chf=amount_chf, fx_status=fx_status, warnings=warnings)


def confirm_equity_sell(conn: Connection, request: EquitySellConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview = preview_equity_sell(conn, request)
    account_id, instrument_id = _position_ids(request.position_id)
    quantity = _decimal(request.quantity, "quantity")
    price = _decimal(request.price_original, "price_original")
    fees = _decimal_non_negative(request.fees_original, "fees_original")
    gross = quantity * price
    net = gross - fees
    fx_status, fx_rate, fx_source, _ = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.trade_date)
    gross_chf = _chf_amount(_format_decimal(gross), Decimal(fx_rate) if fx_rate else None)
    fee_chf = _chf_amount(_format_decimal(fees), Decimal(fx_rate) if fx_rate else None)
    net_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    tx_id = _uuid("tx")
    ts = _now()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, price_original, gross_amount_original, fee_original, net_amount_original, currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf, fee_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, 'sell', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_sale', 1, ?, ?, ?)
    """, (tx_id, account_id, instrument_id, request.trade_date, _format_decimal(-quantity), _format_decimal(price), _format_decimal(gross), _format_decimal(fees), _format_decimal(net), request.currency.upper(), fx_rate, fx_source, fx_status, gross_chf, fee_chf, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    if _cash_account_exists(conn, request.cash_account_id):
        conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.trade_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, f"vue_equity_sale:{tx_id}", "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    audit_id = _audit(conn, action="equity_sell_confirm", entity_type="position", entity_id=request.position_id, payload={**request.model_dump(), "net_amount_original": _format_decimal(net), "gross_amount_original": _format_decimal(gross)}, note=request.note, quality_status="ok" if fx_status != "missing" else "missing_fx")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=tx_id, audit_id=audit_id, message="Verkauf bestätigt · Audit geschrieben")


def preview_equity_dividend(conn: Connection, request: EquityDividendPreviewRequest) -> PreviewResponse:
    _position_ids(request.position_id)
    gross = _decimal(request.gross_amount, "gross_amount")
    taxes = _decimal_non_negative(request.swiss_tax, "swiss_tax") + _decimal_non_negative(request.foreign_tax, "foreign_tax") + _decimal_non_negative(request.other_deductions, "other_deductions")
    net = gross - taxes
    if net < 0:
        raise HTTPException(status_code=422, detail="deductions exceed gross dividend")
    if not _cash_account_exists(conn, request.cash_account_id):
        raise HTTPException(status_code=404, detail="Cash account not found")
    fx_status, fx_rate, _fx_source, fx_warning = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.payment_date)
    warnings = [fx_warning] if fx_warning else []
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="income", summary=f"Dividende netto {request.currency.upper()} {_format_decimal(net)}", amount_chf=_chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None), fx_status=fx_status, warnings=warnings)


def confirm_equity_dividend(conn: Connection, request: EquityDividendConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview = preview_equity_dividend(conn, request)
    account_id, instrument_id = _position_ids(request.position_id)
    gross = _decimal(request.gross_amount, "gross_amount")
    taxes = _decimal_non_negative(request.swiss_tax, "swiss_tax") + _decimal_non_negative(request.foreign_tax, "foreign_tax") + _decimal_non_negative(request.other_deductions, "other_deductions")
    net = gross - taxes
    fx_status, fx_rate, fx_source, _ = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.payment_date)
    gross_chf = _chf_amount(_format_decimal(gross), Decimal(fx_rate) if fx_rate else None)
    tax_chf = _chf_amount(_format_decimal(taxes), Decimal(fx_rate) if fx_rate else None)
    net_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    tx_id = _uuid("tx")
    ts = _now()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, gross_amount_original, tax_original, net_amount_original, currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf, tax_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, 'dividend', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', 1, ?, ?, ?)
    """, (tx_id, account_id, instrument_id, request.payment_date, _format_decimal(gross), _format_decimal(taxes), _format_decimal(net), request.currency.upper(), fx_rate, fx_source, fx_status, gross_chf, tax_chf, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.payment_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    audit_id = _audit(conn, action="equity_dividend_confirm", entity_type="position", entity_id=request.position_id, payload={**request.model_dump(), "net_amount_original": _format_decimal(net), "tax_original": _format_decimal(taxes)}, note=request.note, quality_status="ok" if fx_status != "missing" else "missing_fx")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=tx_id, audit_id=audit_id, message="Dividende bestätigt · Audit geschrieben")


def cleanup_review_artifacts_preview(conn: Connection) -> CleanupPreviewResponse:
    plan = identify_review_test_equity_artifacts(conn)
    warnings = []
    if plan.artifact_count:
        warnings.append("Review-/Test-Artefakte werden archiviert; manuelle Positionen bleiben erhalten.")
    return CleanupPreviewResponse(accounts=len(plan.account_ids), instruments=len(plan.instrument_ids), transactions=len(plan.transaction_ids), alerts=len(plan.alert_ids), manual_positions_preserved=plan.manual_position_count, warnings=warnings)


def cleanup_review_artifacts_confirm(conn: Connection, request: GenericConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    plan = identify_review_test_equity_artifacts(conn)
    audit_id = apply_review_test_equity_cleanup(conn, plan=plan, note=request.note or "Vue v1.2 review/test artifact cleanup")
    return ConfirmResponse(status="confirmed", entity_id="equity_review_test_artifacts", audit_id=audit_id, message="Review-/Test-Artefakte archiviert")


def preview_remove_position(conn: Connection, position_id: str) -> RemovePositionPreviewResponse:
    account_id, instrument_id = position_id.split(":", 1)
    rows = conn.execute(
        """
        SELECT transaction_id, transaction_type, source_type, is_voided
        FROM transactions
        WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0
        ORDER BY created_at
        """,
        (account_id, instrument_id),
    ).fetchall()
    safe_source_types = {"vue_manual_position", "manual_dashboard", "manual_initial_snapshot", "manual_snapshot", "vue_manual_snapshot"}
    safe_tx_types = {"initial_position_snapshot", "initial_snapshot", "manual_adjustment"}
    safe = len(rows) == 1 and rows[0]["transaction_type"] in safe_tx_types and rows[0]["source_type"] in safe_source_types
    reason = "Ein manueller Initial Snapshot kann storniert werden." if safe else "Position hat Historie oder stammt nicht eindeutig aus einem manuellen Initial Snapshot. Nur Storno-/Korrekturworkflow erlaubt."
    return RemovePositionPreviewResponse(position_id=position_id, transaction_count=len(rows), safe_to_remove=safe, reason=reason, affected_transactions=1 if safe else len(rows), consequences=["Transaktion wird als storniert markiert", "Audit-Eintrag wird geschrieben", "Dashboard liest danach den aktualisierten Bestand"] if safe else ["Kein hartes Löschen", "Bitte Korrektur-/Storno-Workflow verwenden"])


def confirm_remove_position(conn: Connection, position_id: str, request: GenericConfirmRequest) -> ConfirmResponse:
    if not request.confirm or request.confirmation_text != "ENTFERNEN":
        raise HTTPException(status_code=400, detail="Confirmation text ENTFERNEN required")
    preview = preview_remove_position(conn, position_id)
    if not preview.safe_to_remove:
        raise HTTPException(status_code=409, detail=preview.reason)
    account_id, instrument_id = position_id.split(":", 1)
    row = conn.execute("SELECT transaction_id FROM transactions WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0", (account_id, instrument_id)).fetchone()
    now = _now()
    conn.execute("UPDATE transactions SET is_voided=1, voided_at=?, void_reason=?, voided_by='vue_dashboard', updated_at=? WHERE transaction_id=?", (now, request.note or "Position entfernen", now, row["transaction_id"]))
    audit_id = _audit(conn, action="position_remove_confirm", entity_type="position", entity_id=position_id, payload={"transaction_id": row["transaction_id"], "position_id": position_id}, note=request.note or "Position entfernt", quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=position_id, audit_id=audit_id, message="Position storniert")


def preview_wallet(conn: Connection, request: ContainerPreviewRequest) -> PreviewResponse:
    name = (request.name or "").strip()
    if not name:
        raise HTTPException(status_code=422, detail="Wallet name required")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="crypto_wallet", summary=f"Wallet erstellen: {name}", fx_status="not_needed", warnings=[])


def confirm_wallet(conn: Connection, request: ContainerConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    name = (request.name or "").strip()
    if not name:
        raise HTTPException(status_code=422, detail="Wallet name required")
    wallet_id = _uuid("wallet")
    now = _now()
    conn.execute("INSERT INTO crypto_wallets(wallet_id, wallet_name, wallet_type, platform_provider, is_active, notes, created_at) VALUES (?, ?, ?, ?, 1, ?, ?)", (wallet_id, name, (request.wallet_type or "Sonstiges"), request.platform_name, request.note, now))
    audit_id = _audit(conn, action="wallet_create_confirm", entity_type="crypto_wallet", entity_id=wallet_id, payload={"name": name, "wallet_type": request.wallet_type}, note=request.note, quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=wallet_id, audit_id=audit_id, message="Wallet erstellt")


def confirm_delete_wallet(conn: Connection, wallet_id: str, request: GenericConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    holdings = int(conn.execute("SELECT COUNT(*) FROM crypto_holdings WHERE wallet_id=?", (wallet_id,)).fetchone()[0] or 0)
    now = _now()
    if holdings == 0:
        conn.execute("DELETE FROM crypto_wallets WHERE wallet_id=?", (wallet_id,))
        action = "wallet_delete_confirm"; message = "Wallet gelöscht"
    else:
        conn.execute("UPDATE crypto_wallets SET is_active=0, updated_at=? WHERE wallet_id=?", (now, wallet_id))
        action = "wallet_deactivate_confirm"; message = "Wallet deaktiviert"
    audit_id = _audit(conn, action=action, entity_type="crypto_wallet", entity_id=wallet_id, payload={"holdings": holdings}, note=request.note, quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=wallet_id, audit_id=audit_id, message=message)


def preview_account(conn: Connection, request: ContainerPreviewRequest) -> PreviewResponse:
    name = (request.account_name or request.name or "").strip()
    if not name:
        raise HTTPException(status_code=422, detail="Account name required")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="account", summary=f"Konto erstellen: {name}", fx_status="not_needed", warnings=[])


def _ensure_platform(conn: Connection, name: str, *, account_type: str, currency: str) -> str:
    row = conn.execute("SELECT platform_id FROM platforms WHERE lower(name)=lower(?)", (name,)).fetchone()
    if row:
        return row["platform_id"]
    platform_id = stable_id("platform", name)
    conn.execute("INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, is_active, notes, created_at) VALUES (?, ?, ?, 'CH', ?, 1, 'Vue manual account container', ?)", (platform_id, name, "broker" if account_type == "brokerage" else "bank", currency.upper(), _now()))
    return platform_id


def confirm_account(conn: Connection, request: ContainerConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    name = (request.account_name or request.name or "").strip()
    if not name:
        raise HTTPException(status_code=422, detail="Account name required")
    account_type = (request.account_type or "brokerage").lower()
    platform_id = _ensure_platform(conn, request.platform_name or "Anderes Konto", account_type=account_type, currency=request.currency)
    account_id = _uuid("acct")
    now = _now()
    conn.execute("INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, is_active, notes, created_at) VALUES (?, ?, ?, ?, ?, 1, ?, ?)", (account_id, platform_id, name, account_type, request.currency.upper(), request.note, now))
    audit_id = _audit(conn, action="account_create_confirm", entity_type="account", entity_id=account_id, payload={"account_name": name, "account_type": account_type}, note=request.note, quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=account_id, audit_id=audit_id, message="Konto erstellt")


def preview_account_value(conn: Connection, account_id: str, request: AccountValuePreviewRequest) -> PreviewResponse:
    if not _is_manual_total_value_account(conn, account_id):
        raise HTTPException(status_code=422, detail="Account is not a manually valued managed portfolio")
    value = _decimal(request.total_value_chf, "total_value_chf")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="managed_portfolio", summary=f"Gesamtwert CHF per {request.valuation_date}", amount_chf=optional_decimal_text(str(value), 2), fx_status="not_needed", warnings=[])


def confirm_account_value(conn: Connection, account_id: str, request: AccountValueConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview_account_value(conn, account_id, request)
    value = _decimal(request.total_value_chf, "total_value_chf")
    snapshot_id = _uuid("acctval")
    now = _now()
    conn.execute(
        """
        INSERT INTO account_value_snapshots(snapshot_id, account_id, valuation_date, total_value_chf, currency, source_type, quality_status, notes, created_at)
        VALUES (?, ?, ?, ?, 'CHF', 'manual_total_value', 'ok', ?, ?)
        """,
        (snapshot_id, account_id, request.valuation_date, optional_decimal_text(str(value), 2), request.note, now),
    )
    audit_id = _audit(conn, action="account_value_confirm", entity_type="account", entity_id=account_id, payload={"snapshot_id": snapshot_id, "valuation_date": request.valuation_date}, note=request.note, quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=snapshot_id, audit_id=audit_id, message="Gesamtwert gespeichert · Audit geschrieben")


def confirm_deactivate_account(conn: Connection, account_id: str, request: GenericConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    has_history = int(conn.execute("SELECT COUNT(*) FROM transactions WHERE account_id=?", (account_id,)).fetchone()[0] or 0) + int(conn.execute("SELECT COUNT(*) FROM cash_balances WHERE account_id=?", (account_id,)).fetchone()[0] or 0)
    now = _now()
    conn.execute("UPDATE accounts SET is_active=0, updated_at=? WHERE account_id=?", (now, account_id))
    action = "account_deactivate_confirm"
    message = "Konto deaktiviert" if has_history else "Konto deaktiviert"
    audit_id = _audit(conn, action=action, entity_type="account", entity_id=account_id, payload={"history_rows": has_history}, note=request.note, quality_status="ok")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=account_id, audit_id=audit_id, message=message)
