"""
ch14_dvp_settlement.py
Vol I · Chapter 14 — DVP Settlement Lifecycle
==============================================
A minimal Python simulation of the five-phase DVP settlement
lifecycle: trade execution, confirmation, clearing/netting,
delivery-versus-payment settlement, and finality.

No external dependencies — runs on Python 3.8+.
"""

import hashlib
import json
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Dict, List, Optional
from enum import Enum


# ── Enums ─────────────────────────────────────────────────────────────────────

class TradeStatus(str, Enum):
    EXECUTED  = "EXECUTED"
    CONFIRMED = "CONFIRMED"
    CLEARED   = "CLEARED"
    SETTLED   = "SETTLED"
    FAILED    = "FAILED"

class InstructionStatus(str, Enum):
    PENDING  = "PENDING"
    MATCHED  = "MATCHED"
    SETTLED  = "SETTLED"
    FAILED   = "FAILED"


# ── Data classes ──────────────────────────────────────────────────────────────

@dataclass
class Trade:
    """A bilateral securities trade as reported by an exchange."""
    trade_id:        str
    isin:            str
    quantity:        int
    price:           float
    currency:        str
    buyer_bic:       str
    seller_bic:      str
    trade_date:      str
    settlement_date: str
    status:          TradeStatus = TradeStatus.EXECUTED

    @property
    def gross_amount(self) -> float:
        return round(self.quantity * self.price, 2)


@dataclass
class NetPosition:
    """Net obligation for one participant after multilateral netting."""
    participant_bic: str
    isin:            str
    net_securities:  int    # positive = receive, negative = deliver
    net_cash:        float  # positive = pay, negative = receive


@dataclass
class SettlementInstruction:
    """A CSD-level delivery-vs-payment instruction."""
    instruction_id:  str
    isin:            str
    quantity:        int
    amount:          float
    currency:        str
    delivering_bic:  str
    receiving_bic:   str
    settlement_date: str
    status:          InstructionStatus = InstructionStatus.PENDING
    settled_at:      Optional[str]     = None
    settlement_hash: Optional[str]     = None


@dataclass
class ParticipantAccount:
    """Simplified CSD account holding securities and cash."""
    bic:          str
    name:         str
    securities:   Dict[str, int] = field(default_factory=dict)
    cash_balance: float          = 0.0

    def has_securities(self, isin: str, qty: int) -> bool:
        return self.securities.get(isin, 0) >= qty

    def has_cash(self, amount: float) -> bool:
        return self.cash_balance >= amount

    def deliver_securities(self, isin: str, qty: int) -> None:
        self.securities[isin] = self.securities.get(isin, 0) - qty

    def receive_securities(self, isin: str, qty: int) -> None:
        self.securities[isin] = self.securities.get(isin, 0) + qty

    def pay_cash(self, amount: float) -> None:
        self.cash_balance -= round(amount, 2)

    def receive_cash(self, amount: float) -> None:
        self.cash_balance += round(amount, 2)


# ── Phase 1: Trade Execution ──────────────────────────────────────────────────

def execute_trades(raw_trades: List[dict]) -> List[Trade]:
    """Phase 1 — Exchange matching engine output."""
    print("\n" + "="*60)
    print("PHASE 1: TRADE EXECUTION")
    print("="*60)
    trades = []
    for t in raw_trades:
        trade = Trade(**t)
        trades.append(trade)
        print(f"  [EXECUTED] {trade.trade_id}: {trade.isin} "
              f"{trade.quantity:,} @ {trade.price:.2f} {trade.currency} "
              f"| {trade.buyer_bic} ← {trade.seller_bic}")
    return trades


# ── Phase 2: Trade Confirmation ───────────────────────────────────────────────

def confirm_trades(trades: List[Trade]) -> List[Trade]:
    """
    Phase 2 — Both counterparties affirm trade details.
    In production this involves bilateral matching via SWIFT or OMGEO.
    """
    print("\n" + "="*60)
    print("PHASE 2: TRADE CONFIRMATION")
    print("="*60)
    for trade in trades:
        trade.status = TradeStatus.CONFIRMED
        print(f"  [CONFIRMED] {trade.trade_id}: "
              f"{trade.buyer_bic} + {trade.seller_bic} — details affirmed")
    return trades


# ── Phase 3: Clearing & Netting ───────────────────────────────────────────────

def clear_and_net(trades: List[Trade]) -> List[NetPosition]:
    """
    Phase 3 — Multilateral netting.
    Reduces gross obligations to a single net position per participant/ISIN.
    """
    print("\n" + "="*60)
    print("PHASE 3: CLEARING & NETTING")
    print("="*60)

    sec_pos:  Dict[tuple, int]   = {}   # (bic, isin) -> net qty
    cash_pos: Dict[str,   float] = {}   # bic -> net cash

    for trade in trades:
        buyer, seller = trade.buyer_bic, trade.seller_bic
        isin, qty     = trade.isin, trade.quantity
        amount        = trade.gross_amount

        sec_pos[(buyer,  isin)] = sec_pos.get((buyer,  isin), 0) + qty
        sec_pos[(seller, isin)] = sec_pos.get((seller, isin), 0) - qty
        cash_pos[buyer]  = cash_pos.get(buyer,  0.0) - amount
        cash_pos[seller] = cash_pos.get(seller, 0.0) + amount
        trade.status = TradeStatus.CLEARED

    net_positions: List[NetPosition] = []
    for (bic, isin), net_sec in sec_pos.items():
        net_cash = cash_pos.get(bic, 0.0)
        net_positions.append(NetPosition(bic, isin, net_sec, net_cash))
        direction = "RECEIVE" if net_sec > 0 else "DELIVER"
        print(f"  [NET] {bic}: {direction} {abs(net_sec):,} {isin} "
              f"| cash: {net_cash:+,.2f} {trades[0].currency}")

    return net_positions


# ── Phase 4: DVP Settlement ───────────────────────────────────────────────────

def build_dvp_instructions(
    net_positions: List[NetPosition],
    currency: str = "EUR"
) -> List[SettlementInstruction]:
    """Build bilateral DVP instructions from net positions."""
    instructions = []
    by_isin: Dict[str, List[NetPosition]] = {}
    for np in net_positions:
        by_isin.setdefault(np.isin, []).append(np)

    for isin, positions in by_isin.items():
        receivers  = [p for p in positions if p.net_securities > 0]
        deliverers = [p for p in positions if p.net_securities < 0]
        for rcv in receivers:
            for dlv in deliverers:
                instructions.append(SettlementInstruction(
                    instruction_id  = f"INSTR-{isin[:4]}-{dlv.participant_bic[:3]}-{rcv.participant_bic[:3]}",
                    isin            = isin,
                    quantity        = abs(dlv.net_securities),
                    amount          = abs(rcv.net_cash),
                    currency        = currency,
                    delivering_bic  = dlv.participant_bic,
                    receiving_bic   = rcv.participant_bic,
                    settlement_date = str(date.today()),
                ))
    return instructions


def settle_dvp(
    instructions: List[SettlementInstruction],
    accounts: Dict[str, ParticipantAccount]
) -> List[SettlementInstruction]:
    """
    Phase 4 — Atomic DVP settlement at the CSD.
    Both legs transfer simultaneously or not at all.
    """
    print("\n" + "="*60)
    print("PHASE 4: DVP SETTLEMENT")
    print("="*60)

    for instr in instructions:
        dlv = accounts.get(instr.delivering_bic)
        rcv = accounts.get(instr.receiving_bic)

        if not dlv or not rcv:
            instr.status = InstructionStatus.FAILED
            print(f"  [FAILED]  {instr.instruction_id}: unknown participant")
            continue

        if not dlv.has_securities(instr.isin, instr.quantity):
            instr.status = InstructionStatus.FAILED
            print(f"  [FAILED]  {instr.instruction_id}: {instr.delivering_bic} "
                  f"insufficient securities")
            continue

        if not rcv.has_cash(instr.amount):
            instr.status = InstructionStatus.FAILED
            print(f"  [FAILED]  {instr.instruction_id}: {instr.receiving_bic} "
                  f"insufficient cash")
            continue

        # ── Atomic transfer (both legs or none) ───────────
        dlv.deliver_securities(instr.isin, instr.quantity)
        rcv.receive_securities(instr.isin, instr.quantity)
        rcv.pay_cash(instr.amount)
        dlv.receive_cash(instr.amount)
        # ──────────────────────────────────────────────────

        instr.status     = InstructionStatus.SETTLED
        instr.settled_at = datetime.utcnow().isoformat() + "Z"
        instr.settlement_hash = hashlib.sha256(
            json.dumps({
                "id": instr.instruction_id, "isin": instr.isin,
                "qty": instr.quantity, "amount": instr.amount,
                "ts": instr.settled_at,
            }, sort_keys=True).encode()
        ).hexdigest()

        print(f"  [SETTLED] {instr.instruction_id}: "
              f"{instr.quantity:,} {instr.isin} "
              f"{instr.delivering_bic} → {instr.receiving_bic} "
              f"| {instr.amount:,.2f} {instr.currency} "
              f"| hash: {instr.settlement_hash[:16]}…")

    return instructions


# ── Phase 5: Finality ─────────────────────────────────────────────────────────

def confirm_finality(
    instructions: List[SettlementInstruction],
    accounts: Dict[str, ParticipantAccount]
) -> None:
    """Phase 5 — Irrevocable transfer confirmed; post-settlement positions."""
    print("\n" + "="*60)
    print("PHASE 5: SETTLEMENT FINALITY")
    print("="*60)
    settled = sum(1 for i in instructions if i.status == InstructionStatus.SETTLED)
    failed  = sum(1 for i in instructions if i.status == InstructionStatus.FAILED)
    print(f"  Settled: {settled}   Failed: {failed}")
    print()
    for acc in sorted(accounts.values(), key=lambda a: a.bic):
        print(f"  [{acc.bic}] {acc.name}")
        for isin, qty in sorted(acc.securities.items()):
            print(f"    {isin}: {qty:>10,} securities")
        print(f"    Cash:  {acc.cash_balance:>14,.2f} EUR")


# ── Demo ──────────────────────────────────────────────────────────────────────

if __name__ == "__main__":

    accounts = {
        "FRBNPPARIBXXX": ParticipantAccount(
            bic="FRBNPPARIBXXX", name="BNP Paribas Securities",
            securities={"FR0010242511": 50_000},
            cash_balance=2_000_000.00,
        ),
        "FRSOCGENXXX": ParticipantAccount(
            bic="FRSOCGENXXX", name="Société Générale Gestion",
            securities={"DE0005140008": 20_000},
            cash_balance=3_500_000.00,
        ),
        "DEGDBKFFXXX": ParticipantAccount(
            bic="DEGDBKFFXXX", name="Deutsche Bank Frankfurt",
            securities={"DE0005140008": 30_000},
            cash_balance=1_800_000.00,
        ),
    }

    raw_trades = [
        dict(trade_id="TRD-20260115-001", isin="FR0010242511", quantity=10_000,
             price=65.24, currency="EUR", buyer_bic="FRSOCGENXXX",
             seller_bic="FRBNPPARIBXXX", trade_date="2026-01-15",
             settlement_date="2026-01-17"),
        dict(trade_id="TRD-20260115-002", isin="DE0005140008", quantity=5_000,
             price=63.75, currency="EUR", buyer_bic="FRBNPPARIBXXX",
             seller_bic="FRSOCGENXXX", trade_date="2026-01-15",
             settlement_date="2026-01-17"),
        dict(trade_id="TRD-20260115-003", isin="DE0005140008", quantity=3_000,
             price=63.80, currency="EUR", buyer_bic="FRBNPPARIBXXX",
             seller_bic="DEGDBKFFXXX", trade_date="2026-01-15",
             settlement_date="2026-01-17"),
    ]

    trades       = execute_trades(raw_trades)
    trades       = confirm_trades(trades)
    net_pos      = clear_and_net(trades)
    instructions = build_dvp_instructions(net_pos)
    instructions = settle_dvp(instructions, accounts)
    confirm_finality(instructions, accounts)
