"""Schemi di peso per i portafogli. Ogni funzione ritorna {sleeve_id: peso} con somma 1."""
from __future__ import annotations

import numpy as np
import pandas as pd

_PREFIX = [("PR_", "PAIRS"), ("SH_", "SHAPE"), ("TSM", "TSM"), ("MR", "FADE"), ("XS", "XSEC")]


def family_of(sleeve_id: str) -> str:
    for pre, fam in _PREFIX:
        if sleeve_id.startswith(pre):
            return fam
    return "HONEST"


def _normalize(w: dict[str, float]) -> dict[str, float]:
    tot = sum(w.values())
    return {k: (v / tot if tot > 0 else 0.0) for k, v in w.items()}


def equal(ids: list[str]) -> dict[str, float]:
    n = len(ids)
    return {i: 1.0 / n for i in ids} if n else {}


def manual(ids: list[str], weights: dict[str, float]) -> dict[str, float]:
    return _normalize({i: float(weights.get(i, 0.0)) for i in ids})


def cap(ids: list[str], caps: dict[str, float]) -> dict[str, float]:
    """Equal-weight con tetto al peso AGGREGATO di una famiglia; l'eccesso ridistribuito
    pro-quota alle famiglie non cappate (iterativo finché tutti i cap sono rispettati)."""
    w = equal(ids)
    fam = {i: family_of(i) for i in ids}
    for _ in range(10):
        over = {}
        for f, lim in caps.items():
            members = [i for i in ids if fam[i] == f]
            cur = sum(w[i] for i in members)
            if cur > lim + 1e-12 and members:
                over[f] = (members, lim, cur)
        if not over:
            break
        free_ids = [i for i in ids if fam[i] not in caps]
        freed = 0.0
        for f, (members, lim, cur) in over.items():
            scale = lim / cur
            for i in members:
                freed += w[i] * (1 - scale)
                w[i] *= scale
        if free_ids and freed > 0:
            add = freed / len(free_ids)
            for i in free_ids:
                w[i] += add
        else:
            break
    return _normalize(w)


def inverse_vol(ids: list[str], returns_df: pd.DataFrame, lookback: int) -> dict[str, float]:
    sub = returns_df[ids].iloc[-lookback:]
    vol = sub.std()
    inv = {i: (1.0 / vol[i] if vol[i] and vol[i] > 0 else 0.0) for i in ids}
    return _normalize(inv)


def cluster_rp(ids: list[str], clusters: dict[str, str],
               returns_df: pd.DataFrame, lookback: int) -> dict[str, float]:
    """Equal fra i cluster naturali, poi inverse-vol dentro ogni cluster."""
    groups: dict[str, list[str]] = {}
    for i in ids:
        groups.setdefault(clusters.get(i, i), []).append(i)
    per = 1.0 / len(groups) if groups else 0.0
    w: dict[str, float] = {}
    for members in groups.values():
        iv = inverse_vol(members, returns_df, lookback)
        for i in members:
            w[i] = per * iv[i]
    return _normalize(w)


def weight_vector(scheme: str, ids: list[str], returns_df: pd.DataFrame | None = None,
                  *, weights: dict | None = None, caps: dict | None = None,
                  clusters: dict | None = None, lookback: int = 90) -> dict[str, float]:
    if scheme == "equal":
        return equal(ids)
    if scheme == "manual":
        return manual(ids, weights or {})
    if scheme == "cap":
        return cap(ids, caps or {})
    if scheme == "inverse_vol":
        return inverse_vol(ids, returns_df, lookback)
    if scheme == "cluster_rp":
        return cluster_rp(ids, clusters or {}, returns_df, lookback)
    raise ValueError(f"schema peso sconosciuto: {scheme}")
