"""Executable research contract for K-ArtSell Aegis sell/reentry policies.

This module is intentionally dependency-free. It is not a production trading engine.
It exists to make policy ordering, units, invariants, and golden vectors reproducible.
"""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal, ROUND_HALF_EVEN
from enum import Enum
from typing import Iterable, Optional, Protocol

ZERO = Decimal("0")
ONE = Decimal("1")


class SellAction(str, Enum):
    HOLD = "HOLD"
    PARTIAL_SELL = "PARTIAL_SELL"
    FULL_SELL = "FULL_SELL"


@dataclass(frozen=True)
class SellInput:
    position_lot_id: str
    cycle_id: str
    evidence_id: str
    dataset_id: str
    model_version: str
    config_version: str
    code_sha: str
    as_of: datetime
    published_at_cutoff: datetime
    current_portfolio_weight: Decimal
    strategic_core_floor_weight: Decimal
    hard_impairment_approved: bool = False
    capital_floor_breached: bool = False
    survival_sell_ratio_of_lot: Decimal = ZERO
    gap_below_floor_atr: Decimal = ZERO
    consecutive_close_breaches: int = 0
    cooldown_satisfied: bool = True
    concentration_sell_ratio_of_lot: Decimal = ZERO
    opportunity_edge_lower_bound: Decimal = ZERO
    opportunity_sell_ratio_of_lot: Decimal = ZERO

    def validate(self) -> None:
        if self.published_at_cutoff > self.as_of:
            raise ValueError("published_at_cutoff cannot be later than as_of")
        for name, value in (
            ("current_portfolio_weight", self.current_portfolio_weight),
            ("strategic_core_floor_weight", self.strategic_core_floor_weight),
            ("survival_sell_ratio_of_lot", self.survival_sell_ratio_of_lot),
            ("concentration_sell_ratio_of_lot", self.concentration_sell_ratio_of_lot),
            ("opportunity_sell_ratio_of_lot", self.opportunity_sell_ratio_of_lot),
        ):
            if not ZERO <= value <= ONE:
                raise ValueError(f"{name} must be between 0 and 1")
        if self.strategic_core_floor_weight > self.current_portfolio_weight:
            raise ValueError("strategic core cannot exceed current portfolio weight")
        if self.gap_below_floor_atr < ZERO:
            raise ValueError("gap_below_floor_atr cannot be negative")
        if self.consecutive_close_breaches < 0:
            raise ValueError("consecutive_close_breaches cannot be negative")

    def max_sell_ratio_preserving_core(self) -> Decimal:
        if self.current_portfolio_weight <= ZERO:
            return ZERO
        sellable_weight = max(ZERO, self.current_portfolio_weight - self.strategic_core_floor_weight)
        return min(ONE, sellable_weight / self.current_portfolio_weight)

    def target_weight_after(self, ratio: Decimal) -> Decimal:
        bounded = min(ONE, max(ZERO, ratio))
        return (self.current_portfolio_weight * (ONE - bounded)).quantize(
            Decimal("0.00000001"), rounding=ROUND_HALF_EVEN
        )


@dataclass(frozen=True)
class SellDecision:
    action: SellAction
    sell_ratio_of_lot: Decimal
    target_portfolio_weight_after: Decimal
    policy_id: str
    priority: int
    reason_code: str
    reentry_eligible: bool


class SellPolicy(Protocol):
    priority: int
    policy_id: str

    def evaluate(self, value: SellInput) -> Optional[SellDecision]: ...


class HardImpairmentPolicy:
    priority = 1000
    policy_id = "ALG-SELL-001"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.hard_impairment_approved:
            return None
        return _decision(value, ONE, self.policy_id, self.priority, "HARD_IMPAIRMENT", False)


class PortfolioSurvivalPolicy:
    priority = 900
    policy_id = "ALG-SELL-PORT-001"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.capital_floor_breached or not value.cooldown_satisfied:
            return None
        ratio = min(ONE, max(ZERO, value.survival_sell_ratio_of_lot))
        return None if ratio == ZERO else _decision(
            value, ratio, self.policy_id, self.priority, "PORTFOLIO_SURVIVAL", True
        )


class GapFloorPolicy:
    priority = 800
    policy_id = "ALG-SELL-002"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.cooldown_satisfied or value.gap_below_floor_atr < Decimal("1.5"):
            return None
        ratio = min(Decimal("0.40"), value.max_sell_ratio_preserving_core())
        return None if ratio == ZERO else _decision(
            value, ratio, self.policy_id, self.priority, "GAP_FLOOR_BREACH", True
        )


class TwoClosePolicy:
    priority = 700
    policy_id = "ALG-SELL-003"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.cooldown_satisfied or value.consecutive_close_breaches < 2:
            return None
        ratio = min(Decimal("0.20"), value.max_sell_ratio_preserving_core())
        return None if ratio == ZERO else _decision(
            value, ratio, self.policy_id, self.priority, "TWO_CLOSE_FLOOR_BREACH", True
        )


class ConcentrationPolicy:
    priority = 600
    policy_id = "ALG-SELL-004"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.cooldown_satisfied:
            return None
        ratio = min(value.concentration_sell_ratio_of_lot, value.max_sell_ratio_preserving_core())
        return None if ratio <= ZERO else _decision(
            value, ratio, self.policy_id, self.priority, "CONCENTRATION_OR_LIQUIDITY", True
        )


class OpportunityCostPolicy:
    priority = 500
    policy_id = "ALG-SELL-005"

    def evaluate(self, value: SellInput) -> Optional[SellDecision]:
        if not value.cooldown_satisfied or value.opportunity_edge_lower_bound <= ZERO:
            return None
        requested = min(Decimal("0.25"), max(Decimal("0.10"), value.opportunity_sell_ratio_of_lot))
        ratio = min(requested, value.max_sell_ratio_preserving_core())
        return None if ratio <= ZERO else _decision(
            value, ratio, self.policy_id, self.priority, "OPPORTUNITY_REPLACEMENT", True
        )


class SellPolicyChain:
    def __init__(self, policies: Iterable[SellPolicy]):
        self._policies = tuple(sorted(policies, key=lambda p: (-p.priority, p.policy_id)))

    def evaluate(self, value: SellInput) -> SellDecision:
        value.validate()
        for policy in self._policies:
            decision = policy.evaluate(value)
            if decision is not None:
                return decision
        return SellDecision(
            SellAction.HOLD,
            ZERO,
            value.current_portfolio_weight,
            "ALG-HOLD-001",
            0,
            "NO_SELL_CONDITION",
            False,
        )


def default_chain() -> SellPolicyChain:
    return SellPolicyChain(
        [
            OpportunityCostPolicy(),
            ConcentrationPolicy(),
            TwoClosePolicy(),
            GapFloorPolicy(),
            PortfolioSurvivalPolicy(),
            HardImpairmentPolicy(),
        ]
    )


def _decision(
    value: SellInput,
    ratio: Decimal,
    policy_id: str,
    priority: int,
    reason: str,
    reentry_eligible: bool,
) -> SellDecision:
    action = SellAction.FULL_SELL if ratio == ONE else SellAction.PARTIAL_SELL
    return SellDecision(
        action,
        ratio,
        value.target_weight_after(ratio),
        policy_id,
        priority,
        reason,
        reentry_eligible,
    )
