# -*- coding: utf-8 -*-
"""
安全垫与成本账 (纯函数/纯对象, 无外部依赖, 可单测)
==================================================
职责 (POSITION_MGMT_DESIGN.md §4/§6):
  1. 摊薄成本口径: avg_cost = (累计买入额 − 累计卖出额 − 做T利润) / 当前持股数。
     卖得比成本高、做T赚了钱, 摊薄成本都会下降 —— 安全垫由此增厚。
  2. 安全垫状态机: NONE(<0) / THIN(0~solid) / SOLID(≥solid, 解锁盈利加仓)。
  3. 保垫减仓触发: 垫子峰值 ≥ peak_min 且 现值回吐 ≥ 峰值 × giveback。
  4. 卖出核销次序: T0 → ADD(新→旧) → DCA → FILL → BASE (保底仓纪律)。
"""
from __future__ import annotations


class PositionCost:
    """一只股票的成本账 (摊薄口径)。所有金额单位元, 数量单位股。"""

    def __init__(self):
        self.qty = 0
        self.cum_buy_amt = 0.0
        self.cum_sell_amt = 0.0
        self.t_profit = 0.0

    def buy(self, qty: int, price: float):
        if qty <= 0 or price <= 0:
            raise ValueError(f"非法买入: qty={qty}, price={price}")
        self.qty += qty
        self.cum_buy_amt += qty * price

    def sell(self, qty: int, price: float):
        if qty <= 0 or price <= 0:
            raise ValueError(f"非法卖出: qty={qty}, price={price}")
        if qty > self.qty:
            raise ValueError(f"卖出超持仓: sell={qty} > hold={self.qty}")
        self.qty -= qty
        self.cum_sell_amt += qty * price
        if self.qty == 0:  # 清仓即结账, 防止空仓残留成本影响下一轮
            self.reset_keep_nothing()

    def add_t_profit(self, amount: float):
        """做T已实现利润 (可为负 = T亏损), 直接摊入成本。"""
        self.t_profit += amount

    def reset_keep_nothing(self):
        self.qty = 0
        self.cum_buy_amt = 0.0
        self.cum_sell_amt = 0.0
        self.t_profit = 0.0

    @property
    def avg_cost(self):
        """摊薄成本。空仓返回 None; 净成本为负 (卖出已收回全部本金) 时返回 0.0 (垫子视为无限厚)。"""
        if self.qty <= 0:
            return None
        net = self.cum_buy_amt - self.cum_sell_amt - self.t_profit
        return max(net / self.qty, 0.0)

    def cushion(self, price: float):
        """安全垫幅度 = 现价/摊薄成本 − 1。空仓返回 None; 成本≤0 返回大数 (视为极厚)。"""
        c = self.avg_cost
        if c is None:
            return None
        if c <= 0:
            return 9.99
        return price / c - 1.0


def cushion_state(cushion_pct, solid: float = 0.03) -> str:
    """垫子状态: NONE(<0) / THIN(0~solid) / SOLID(≥solid)。None 视为 NONE。"""
    if cushion_pct is None or cushion_pct < 0:
        return "NONE"
    return "SOLID" if cushion_pct >= solid else "THIN"


def trim_trigger(cushion_peak: float, cushion_now, peak_min: float = 0.06,
                 giveback: float = 0.5) -> bool:
    """保垫减仓触发: 峰值曾 ≥ peak_min 且 现值回吐 ≥ 峰值 × giveback。"""
    if cushion_now is None or cushion_peak is None:
        return False
    if cushion_peak < peak_min:
        return False
    return (cushion_peak - cushion_now) >= cushion_peak * giveback - 1e-12


# 卖出核销优先级 (数值小者先卖): 保底仓纪律
_SELL_ORDER = {"T0": 0, "ADD": 1, "DCA": 2, "FILL": 3, "BASE": 4, "RECON": 5}


def sell_allocation(lots: list, sell_qty: int) -> list:
    """把卖出数量分配到批次: T0 → ADD(新→旧) → DCA → FILL → BASE。

    lots: [{"lot_id", "lot_type", "qty"(未核销数量), "open_date"(YYYYMMDD int 或可比较值)}, ...]
    返回 [{"lot_id", "qty"}...]; 卖出量超过批次总量 → ValueError (调用方应先对账)。
    """
    if sell_qty <= 0:
        return []
    total = sum(l["qty"] for l in lots)
    if sell_qty > total:
        raise ValueError(f"卖出超批次总量: sell={sell_qty} > lots={total} (先对账再动作)")

    def key(l):
        tier = _SELL_ORDER.get(l["lot_type"], 9)
        # ADD 批内部按开仓日新→旧 (负号); 其余批按旧→新
        date_key = -l["open_date"] if l["lot_type"] == "ADD" else l["open_date"]
        return (tier, date_key)

    out, remain = [], sell_qty
    for l in sorted(lots, key=key):
        if remain <= 0:
            break
        take = min(l["qty"], remain)
        if take > 0:
            out.append({"lot_id": l["lot_id"], "qty": take})
            remain -= take
    return out


def dca_stage(loss_pct: float, triggers=(-0.08, -0.15)) -> int:
    """补仓评估档: 返回已触及的最深档序号 (1 起), 未触及任何档返回 0。
    triggers 按由浅到深排列 (如 -0.08, -0.15)。"""
    stage = 0
    for i, t in enumerate(sorted(triggers, reverse=True), start=1):  # 浅档在前
        if loss_pct <= t + 1e-12:
            stage = i
    return stage
