from __future__ import annotations

import hashlib
import json
import uuid
from datetime import UTC, date, datetime
from decimal import Decimal, InvalidOperation
from sqlite3 import Connection, IntegrityError
from typing import Any

from jarvis_finance.audit.log import record_audit_event

VALID_CLASSES = {"cash", "equity", "crypto", "other"}
VALID_CURRENCIES = {"CHF", "EUR", "USD"}


def _now() -> str:
    return datetime.now(UTC).isoformat()


def _decimal(value: object, field: str, errors: list[str]) -> Decimal | None:
    if value in (None, ""):
        return None
    if isinstance(value, float):
        errors.append(f"{field}: muss als exakter Decimal-Text angegeben werden")
        return None
    try:
        result = Decimal(str(value).strip())
    except (InvalidOperation, ValueError):
        errors.append(f"{field}: ungültiger Zahlenwert")
        return None
    if not result.is_finite() or result < 0:
        errors.append(f"{field}: muss eine endliche, nicht-negative Zahl sein")
        return None
    return result


def _decimal_text(value: Decimal | None) -> str | None:
    return None if value is None else format(value.normalize(), "f")


def _canonical_payload(payload: dict[str, Any]) -> dict[str, Any]:
    """Return the exact, JSON-stable policy content that a confirmation commits."""
    result: dict[str, Any] = {}
    for key in (
        "base_currency", "effective_from", "horizon", "objective", "liquidity_reserve",
        "monthly_contribution", "max_single_position_pct", "max_crypto_pct",
        "rebalance_tolerance_pct", "min_transaction_amount", "benchmarks", "restrictions",
    ):
        value = payload.get(key)
        result[key] = [] if key in {"benchmarks", "restrictions"} and value is None else value
    result["allocations"] = sorted(
        [
            {
                "asset_class": str(row.get("asset_class") or ""),
                "target_pct": row.get("target_pct"),
                "lower_pct": row.get("lower_pct"),
                "upper_pct": row.get("upper_pct"),
            }
            for row in (payload.get("allocations") or [])
            if isinstance(row, dict)
        ],
        key=lambda row: str(row["asset_class"]),
    )
    return result


def _payload_hash(payload: dict[str, Any]) -> str:
    return hashlib.sha256(
        json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
    ).hexdigest()


def _validate(payload: dict[str, Any]) -> tuple[list[str], list[dict[str, str]]]:
    errors: list[str] = []
    if payload.get("base_currency") not in VALID_CURRENCIES:
        errors.append("Basiswährung ist ungültig")
    try:
        date.fromisoformat(str(payload.get("effective_from") or ""))
    except ValueError:
        errors.append("Gültigkeitsdatum muss ISO-8601 (YYYY-MM-DD) sein")
    rows = payload.get("allocations") or []
    if not isinstance(rows, list) or not rows:
        errors.append("Mindestens eine Zielallokation ist erforderlich")
        rows = []
    seen: set[str] = set()
    total = Decimal("0")
    normalized: list[dict[str, str]] = []
    for row in rows:
        if not isinstance(row, dict):
            errors.append("Jede Zielallokation muss ein Objekt sein")
            continue
        asset = str(row.get("asset_class") or "")
        if asset not in VALID_CLASSES or asset in seen:
            errors.append("Assetklasse ist ungültig oder doppelt")
            continue
        seen.add(asset)
        target = _decimal(row.get("target_pct"), f"{asset} Zielwert", errors)
        lower = _decimal(row.get("lower_pct"), f"{asset} Untergrenze", errors)
        upper = _decimal(row.get("upper_pct"), f"{asset} Obergrenze", errors)
        if target is None or lower is None or upper is None:
            errors.append(f"{asset}: Zielwert und beide Bandgrenzen sind erforderlich")
            continue
        if any(value > Decimal("100") for value in (target, lower, upper)):
            errors.append(f"{asset}: Prozentwerte dürfen 100 Prozent nicht überschreiten")
        if lower > target or target > upper:
            errors.append(f"{asset}: Zielwert muss innerhalb der Bandgrenzen liegen")
        total += target
        normalized.append({"asset_class": asset, "target_pct": _decimal_text(target) or "0", "lower_pct": _decimal_text(lower) or "0", "upper_pct": _decimal_text(upper) or "0"})
    if rows and total != Decimal("100"):
        errors.append("Zielallokationen müssen exakt 100 Prozent ergeben")
    crypto = next((row for row in normalized if row["asset_class"] == "crypto"), None)
    cap = _decimal(payload.get("max_crypto_pct"), "Maximale Crypto-Quote", errors)
    if cap is not None and cap > Decimal("100"):
        errors.append("Maximale Crypto-Quote darf 100 Prozent nicht überschreiten")
    if crypto and cap is not None and Decimal(crypto["target_pct"]) > cap:
        errors.append("Crypto-Zielallokation überschreitet die maximale Crypto-Quote")
    for field in ("liquidity_reserve", "monthly_contribution", "min_transaction_amount"):
        _decimal(payload.get(field), field, errors)
    for field in ("max_single_position_pct", "rebalance_tolerance_pct"):
        value = _decimal(payload.get(field), field, errors)
        if value is not None and value > Decimal("100"):
            errors.append(f"{field}: darf 100 Prozent nicht überschreiten")
    benchmarks = payload.get("benchmarks") or []
    if not isinstance(benchmarks, list):
        errors.append("Benchmarks müssen als Liste angegeben werden")
    else:
        for index, benchmark in enumerate(benchmarks, start=1):
            field = f"Benchmark {index}"
            if not isinstance(benchmark, dict):
                errors.append(f"{field}: muss ein Objekt sein")
                continue
            unexpected = set(benchmark) - {"reference", "weight"}
            if unexpected:
                errors.append(f"{field}: enthält nicht unterstützte Felder")
            reference = benchmark.get("reference")
            if not isinstance(reference, str) or not reference.strip():
                errors.append(f"{field}: Referenz ist erforderlich")
            if "weight" in benchmark:
                errors.append(f"{field}: Gewichte werden nicht unterstützt")
    return errors, sorted(normalized, key=lambda row: row["asset_class"])


def preview_policy(conn: Connection, payload: dict[str, Any]) -> dict[str, Any]:
    del conn  # Preview is deliberately pure and cannot touch storage.
    canonical = _canonical_payload(payload)
    errors, allocations = _validate(canonical)
    canonical["allocations"] = allocations
    return {
        "preview_id": "policy-preview-" + _payload_hash(canonical)[:24],
        "confirmation_id": "policy-confirm-" + uuid.uuid4().hex,
        "valid": not errors,
        "errors": errors,
        "allocations": allocations,
        "summary": "Policy kann bestätigt werden" if not errors else "Policy enthält Validierungsfehler",
    }


def _load_policy(conn: Connection, *, active_only: bool = True, policy_id: str | None = None) -> dict[str, Any] | None:
    if policy_id:
        row = conn.execute("SELECT * FROM portfolio_policies WHERE policy_id=?", (policy_id,)).fetchone()
    else:
        where = "WHERE is_active=1" if active_only else ""
        row = conn.execute(f"SELECT * FROM portfolio_policies {where} ORDER BY version DESC LIMIT 1").fetchone()
    if not row:
        return None
    result = dict(row)
    for key in ("benchmarks_json", "restrictions_json"):
        result[key.removesuffix("_json")] = json.loads(result.pop(key) or "[]")
    result["is_active"] = bool(result["is_active"])
    result["allocations"] = [dict(item) for item in conn.execute("SELECT asset_class, target_pct, lower_pct, upper_pct FROM portfolio_policy_allocations WHERE policy_id=? ORDER BY asset_class", (row["policy_id"],)).fetchall()]
    return result


def confirm_policy(conn: Connection, payload: dict[str, Any]) -> dict[str, Any]:
    if payload.get("confirm") is not True:
        raise ValueError("Explizite Bestätigung ist erforderlich")
    canonical = _canonical_payload(payload)
    preview = preview_policy(conn, canonical)
    if not preview["valid"]:
        raise ValueError("; ".join(preview["errors"]))
    if payload.get("preview_id") != preview["preview_id"]:
        raise ValueError("Vorschau stimmt nicht mit der Bestätigung überein")
    canonical["allocations"] = preview["allocations"]
    confirmation_id = str(payload.get("confirmation_id") or "")
    if not confirmation_id:
        raise ValueError("Bestätigungskennung fehlt")
    fingerprint = _payload_hash(canonical)
    try:
        conn.execute("BEGIN IMMEDIATE")
        existing = conn.execute(
            """SELECT policy_id, version, audit_id, payload_hash
               FROM portfolio_policies WHERE confirmation_id=?""",
            (confirmation_id,),
        ).fetchone()
        if existing:
            if existing["payload_hash"] != fingerprint:
                raise ValueError("Bestätigungskennung wurde mit verändertem Inhalt wiederverwendet")
            conn.rollback()
            return {"policy_id": existing["policy_id"], "version": existing["version"], "audit_id": existing["audit_id"], "idempotent": True}
        prior = conn.execute("SELECT policy_id, version FROM portfolio_policies WHERE is_active=1 ORDER BY version DESC LIMIT 1").fetchone()
        version = int(prior["version"] if prior else 0) + 1
        policy_id = f"policy-{uuid.uuid4().hex}"
        now = _now()
        # Only the active marker changes on an older record; policy content is guarded immutable in SQL.
        if prior:
            conn.execute(
                "UPDATE portfolio_policies SET is_active=0 WHERE policy_id=?",
                (prior["policy_id"],),
            )
        audit_id = record_audit_event(
            conn,
            source="portfolio_policy_api",
            action="portfolio_policy_confirmed",
            entity_type="portfolio_policy",
            entity_id=policy_id,
            new_values={
                "version": version,
                "previous_policy_id": prior["policy_id"] if prior else None,
            },
            confirmed=True,
            created_by="user",
        )
        conn.execute(
            """INSERT INTO portfolio_policies(policy_id, version, is_active, effective_from, previous_policy_id, base_currency, horizon, objective, liquidity_reserve, monthly_contribution, max_single_position_pct, max_crypto_pct, rebalance_tolerance_pct, min_transaction_amount, benchmarks_json, restrictions_json, request_fingerprint, confirmation_id, payload_hash, audit_id, created_at)
               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
            (
                policy_id, version, 1, canonical["effective_from"],
                prior["policy_id"] if prior else None, canonical["base_currency"],
                canonical["horizon"], canonical["objective"],
                canonical["liquidity_reserve"], canonical["monthly_contribution"],
                canonical["max_single_position_pct"], canonical["max_crypto_pct"],
                canonical["rebalance_tolerance_pct"], canonical["min_transaction_amount"],
                json.dumps(canonical["benchmarks"], sort_keys=True),
                json.dumps(canonical["restrictions"], sort_keys=True),
                confirmation_id, confirmation_id, fingerprint, audit_id, now,
            ),
        )
        for row in canonical["allocations"]:
            conn.execute(
                """INSERT INTO portfolio_policy_allocations(
                       allocation_id, policy_id, asset_class, target_pct, lower_pct, upper_pct
                   ) VALUES (?,?,?,?,?,?)""",
                (
                    f"alloc-{uuid.uuid4().hex}", policy_id, row["asset_class"],
                    row["target_pct"], row["lower_pct"], row["upper_pct"],
                ),
            )
        conn.commit()
    except IntegrityError as exc:
        conn.rollback()
        existing = conn.execute(
            """SELECT policy_id, version, audit_id, payload_hash
               FROM portfolio_policies WHERE confirmation_id=?""",
            (confirmation_id,),
        ).fetchone()
        if existing:
            if existing["payload_hash"] != fingerprint:
                raise ValueError(
                    "Bestätigungskennung wurde mit verändertem Inhalt wiederverwendet"
                ) from exc
            return {"policy_id": existing["policy_id"], "version": existing["version"], "audit_id": existing["audit_id"], "idempotent": True}
        raise ValueError("Policy konnte nicht atomar gespeichert werden") from exc
    except Exception:
        conn.rollback()
        raise
    return {"policy_id": policy_id, "version": version, "audit_id": audit_id, "idempotent": False}


def active_policy(conn: Connection) -> dict[str, Any]:
    policy = _load_policy(conn)
    return {"configured": policy is not None, "policy": policy}


def policy_detail(conn: Connection, policy_id: str) -> dict[str, Any] | None:
    """Load one immutable policy version without changing storage state."""
    return _load_policy(conn, active_only=False, policy_id=policy_id)


def policy_history(conn: Connection) -> list[dict[str, Any]]:
    rows = conn.execute("SELECT policy_id, version, is_active, effective_from, previous_policy_id, base_currency, horizon, objective, created_at, audit_id FROM portfolio_policies ORDER BY version DESC").fetchall()
    return [{**dict(row), "is_active": bool(row["is_active"])} for row in rows]


def evaluate_policy(conn: Connection) -> dict[str, Any]:
    policy = _load_policy(conn)
    if not policy:
        return {"configured": False, "data_quality_status": "unavailable", "rows": [], "reason": "Anlagestrategie ist nicht konfiguriert"}
    # No provider, order, FX, or valuation operation occurs here. Existing data does not yet
    # provide a complete, normalized asset-class valuation contract, so no compliance claim is made.
    return {"configured": True, "version": policy["version"], "effective_from": policy["effective_from"], "data_quality_status": "partial", "rows": [{**allocation, "current_pct": None, "status": "not_assessable", "reason": "Aktuelle Werte sind noch nicht vollständig klassifiziert und vergleichbar"} for allocation in policy["allocations"]], "reason": "Keine definitive Policy-Einhaltung bei unvollständigem Datenstand"}
