from __future__ import annotations

import argparse
import json
import time
from collections import Counter
from datetime import datetime, timezone
from dataclasses import dataclass
from typing import Any

from hl1m_cli.copytrade._shared.hyperliquid_constants import DEFAULT_HTTP_BASE_URL

from hl1m_cli.copytrade._shared.sdk_info_client import HyperliquidInfoClient


def _f(v: Any) -> float:
    try:
        return float(v)
    except Exception:
        return 0.0


def _i(v: Any) -> int:
    try:
        return int(v)
    except Exception:
        return 0


def get_pnl_history_daily_from_portfolio(
    *,
    portfolio_data: Any,
    start_ms: int,
    end_ms: int,
    days: int,
) -> list[dict[str, Any]]:
    """
    Select portfolio section by `days` and return raw pnlHistory points.

    Rule:
    - days == 1: use "day"
    - 1 < days <= 7: use "week"
    - 7 < days <= 30: use "month"
    - days > 30: use "allTime"

    Output is NOT aggregated to daily buckets. Each item is:
      {"timeMs": int, "closedPnlUsdc": float}
    """

    # pick section key
    d = int(days)
    if d <= 1:
        want = "day"
    elif d <= 7:
        want = "week"
    elif d <= 30:
        want = "month"
    else:
        want = "alltime"

    section: dict[str, Any] | None = None
    try:
        if isinstance(portfolio_data, list):
            for item in portfolio_data:
                if not (isinstance(item, list) and len(item) == 2):
                    continue
                k = str(item[0] or "").strip()
                v = item[1]
                k_norm = k.replace("_", "").replace("-", "").lower()
                if k_norm == want and isinstance(v, dict):
                    section = v
                    break
    except Exception:
        section = None

    out: list[dict[str, Any]] = []
    try:
        pnl_history = (section or {}).get("pnlHistory") or []
        if isinstance(pnl_history, list):
            for pair in pnl_history:
                if not isinstance(pair, list) or len(pair) < 2:
                    continue
                t_ms = _i(pair[0])
                if t_ms < start_ms or t_ms > end_ms:
                    continue
                out.append({"timeMs": t_ms, "closedPnlUsdc": _f(pair[1])})
    except Exception:
        return []

    out.sort(key=lambda x: int(x.get("timeMs", 0) or 0))
    return out


@dataclass(frozen=True)
class AddressAnalysis:
    user: str
    analysis_start_ms: int
    analysis_end_ms: int
    days: int

    # 1) User state (two scopes)
    # - default: no dex param (default scope; crypto positions)
    withdrawable_usdc: float
    margin_summary_default: dict[str, Any]
    cross_margin_summary_default: dict[str, Any]
    total_position_value_usdc: float
    total_unrealized_pnl_usdc: float
    positions_by_coin: list[dict[str, Any]]
    # - xyz: dex=xyz (HIP-3 xyz token positions)
    withdrawable_usdc_xyz: float
    margin_summary_xyz: dict[str, Any]
    cross_margin_summary_xyz: dict[str, Any]
    total_position_value_usdc_xyz: float
    total_unrealized_pnl_usdc_xyz: float
    positions_by_coin_xyz: list[dict[str, Any]]

    # 2) user fills
    fills_count: int
    realized_pnl_usdc: float
    fees_usdc: float
    win_rate: float | None
    top_coins: list[tuple[str, int]]

    # 3) PnL history (raw portfolio pnlHistory points)
    pnl_history_daily: list[dict[str, Any]]

    # 4) Cash flow (non-funding ledger)
    deposit_usdc: float
    withdraw_usdc: float
    net_cashflow_usdc: float
    ledger_type_counts: dict[str, int]

    def to_dict(self) -> dict[str, Any]:
        return {
            "user": self.user,
            "analysisStartMs": self.analysis_start_ms,
            "analysisEndMs": self.analysis_end_ms,
            "days": self.days,
            "holdings": {
                "default": {
                    "withdrawableUsdc": self.withdrawable_usdc,
                    "marginSummary": self.margin_summary_default,
                    "crossMarginSummary": self.cross_margin_summary_default,
                    "totalPositionValueUsdc": self.total_position_value_usdc,
                    "totalUnrealizedPnlUsdc": self.total_unrealized_pnl_usdc,
                    "positionsByCoin": self.positions_by_coin,
                },
                "dexXyz": {
                    "withdrawableUsdc": self.withdrawable_usdc_xyz,
                    "marginSummary": self.margin_summary_xyz,
                    "crossMarginSummary": self.cross_margin_summary_xyz,
                    "totalPositionValueUsdc": self.total_position_value_usdc_xyz,
                    "totalUnrealizedPnlUsdc": self.total_unrealized_pnl_usdc_xyz,
                    "positionsByCoin": self.positions_by_coin_xyz,
                },
            },
            "fillsCount": self.fills_count,
            "realizedPnlUsdc": self.realized_pnl_usdc,
            "feesUsdc": self.fees_usdc,
            "winRate": self.win_rate,
            "topCoins": [{"coin": c, "fills": n} for c, n in self.top_coins],
            "pnlHistoryDaily": self.pnl_history_daily,
            "cashFlow": {
                "depositUsdc": self.deposit_usdc,
                "withdrawUsdc": self.withdraw_usdc,
                "netCashflowUsdc": self.net_cashflow_usdc,
                "ledgerTypeCounts": self.ledger_type_counts,
            },
        }

    def to_markdown(self) -> str:
        lines: list[str] = []
        lines.append("# 1m-trade Hyperliquid Address Analysis Report")
        lines.append("")
        lines.append("## Summary")
        lines.append(f"- user: `{self.user}`")
        lines.append(f"- range: {self.days}d (UTC)")
        lines.append(
            f"- holdings(default): withdrawable={self.withdrawable_usdc:.6f} USDC, "
            f"unrealizedPnl≈{self.total_unrealized_pnl_usdc:.6f} USDC"
        )
        if self.margin_summary_default:
            acct = self.margin_summary_default.get("accountValue")
            raw = self.margin_summary_default.get("totalRawUsd")
            tntl = self.margin_summary_default.get("totalNtlPos")
            mu = self.margin_summary_default.get("totalMarginUsed")
            lines.append(
                f"- holdings(default) marginSummary: accountValue≈{_f(acct):.6f} USDC, "
                f"totalRawUsd≈{_f(raw):.6f} USDC, totalNtlPos≈{_f(tntl):.6f} USDC, totalMarginUsed≈{_f(mu):.6f} USDC"
            )
        if self.cross_margin_summary_default:
            acct = self.cross_margin_summary_default.get("accountValue")
            raw = self.cross_margin_summary_default.get("totalRawUsd")
            tntl = self.cross_margin_summary_default.get("totalNtlPos")
            mu = self.cross_margin_summary_default.get("totalMarginUsed")
            lines.append(
                f"- holdings(default) crossMargin: accountValue≈{_f(acct):.6f} USDC, "
                f"totalRawUsd≈{_f(raw):.6f} USDC, totalNtlPos≈{_f(tntl):.6f} USDC, totalMarginUsed≈{_f(mu):.6f} USDC"
            )
        lines.append(
            f"- holdings(dex=xyz): withdrawable={self.withdrawable_usdc_xyz:.6f} USDC, "
            f"unrealizedPnl≈{self.total_unrealized_pnl_usdc_xyz:.6f} USDC"
        )
        if self.margin_summary_xyz:
            acct = self.margin_summary_xyz.get("accountValue")
            raw = self.margin_summary_xyz.get("totalRawUsd")
            tntl = self.margin_summary_xyz.get("totalNtlPos")
            mu = self.margin_summary_xyz.get("totalMarginUsed")
            lines.append(
                f"- holdings(dex=xyz) marginSummary: accountValue≈{_f(acct):.6f} USDC, "
                f"totalRawUsd≈{_f(raw):.6f} USDC, totalNtlPos≈{_f(tntl):.6f} USDC, totalMarginUsed≈{_f(mu):.6f} USDC"
            )
        if self.cross_margin_summary_xyz:
            acct = self.cross_margin_summary_xyz.get("accountValue")
            raw = self.cross_margin_summary_xyz.get("totalRawUsd")
            tntl = self.cross_margin_summary_xyz.get("totalNtlPos")
            mu = self.cross_margin_summary_xyz.get("totalMarginUsed")
            lines.append(
                f"- holdings(dex=xyz) crossMargin: accountValue≈{_f(acct):.6f} USDC, "
                f"totalRawUsd≈{_f(raw):.6f} USDC, totalNtlPos≈{_f(tntl):.6f} USDC, totalMarginUsed≈{_f(mu):.6f} USDC"
            )
        lines.append(f"- fills analyzed: **{self.fills_count}**")
        lines.append(f"- realized pnl (closedPnl sum): **{self.realized_pnl_usdc:.6f} USDC**")
        lines.append(f"- fees (fee sum): **{self.fees_usdc:.6f} USDC**")
        if self.win_rate is None:
            lines.append("- win rate (approx): N/A")
        else:
            lines.append(f"- win rate (approx): **{self.win_rate * 100:.2f}%**")
        lines.append("")

        lines.append("## Positions (default dex / crypto)")
        if not self.positions_by_coin:
            lines.append("- (none)")
        else:
            for p in self.positions_by_coin:
                coin = str(p.get("coin", ""))
                upnl = float(p.get("unrealizedPnlUsdc", 0.0) or 0.0)
                pv = float(p.get("positionValueUsdc", 0.0) or 0.0)
                mu = float(p.get("marginUsedUsdc", 0.0) or 0.0)
                lev = p.get("leverage") if isinstance(p.get("leverage"), dict) else {}
                lev_type = lev.get("type", "")
                lev_val = lev.get("value", "")
                entry_px = p.get("entryPx")
                liq_px = p.get("liquidationPx")
                szi = p.get("szi")
                roe = p.get("returnOnEquity")
                lines.append(
                    f"- {coin}: positionValue≈{pv:.6f} USDC, marginUsed≈{mu:.6f} USDC, "
                    f"unrealizedPnl≈{upnl:.6f} USDC, leverage={lev_type}:{lev_val}, "
                    f"entryPx={entry_px}, liquidationPx={liq_px}, szi={szi}, returnOnEquity≈{roe}"
                )
        lines.append("")

        lines.append("## Positions (dex=xyz / HIP-3 xyz)")
        if not self.positions_by_coin_xyz:
            lines.append("- (none)")
        else:
            for p in self.positions_by_coin_xyz:
                coin = str(p.get("coin", ""))
                upnl = float(p.get("unrealizedPnlUsdc", 0.0) or 0.0)
                pv = float(p.get("positionValueUsdc", 0.0) or 0.0)
                mu = float(p.get("marginUsedUsdc", 0.0) or 0.0)
                lev = p.get("leverage") if isinstance(p.get("leverage"), dict) else {}
                lev_type = lev.get("type", "")
                lev_val = lev.get("value", "")
                entry_px = p.get("entryPx")
                liq_px = p.get("liquidationPx")
                szi = p.get("szi")
                roe = p.get("returnOnEquity")
                lines.append(
                    f"- {coin}: positionValue≈{pv:.6f} USDC, marginUsed≈{mu:.6f} USDC, "
                    f"unrealizedPnl≈{upnl:.6f} USDC, leverage={lev_type}:{lev_val}, "
                    f"entryPx={entry_px}, liquidationPx={liq_px}, szi={szi}, returnOnEquity≈{roe}"
                )
        lines.append("")

        lines.append("## Top coins (fills)")
        if not self.top_coins:
            lines.append("- (none)")
        else:
            for coin, n in self.top_coins:
                lines.append(f"- {coin}: {n} fills")
        lines.append("")

        lines.append("## Historical realized PnL (portfolio pnlHistory)")
        if not self.pnl_history_daily:
            lines.append("- (none)")
        else:
            for b in self.pnl_history_daily:
                date = str(b.get("date", "") or "")
                if not date:
                    t_ms = int(b.get("timeMs", 0) or 0)
                    if t_ms > 0:
                        date = datetime.fromtimestamp(t_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
                pnl = float(b.get("closedPnlUsdc", 0.0) or 0.0)
                fee = float(b.get("feesUsdc", 0.0) or 0.0)
                cnt = int(b.get("fills", 0) or 0)
                lines.append(f"- {date}: pnl≈{pnl:.6f} USDC, fees≈{fee:.6f} USDC, fills={cnt}")
        lines.append("")

        lines.append("## Cash flow (non-funding ledger)")
        lines.append(f"- deposits: **{self.deposit_usdc:.6f} USDC**")
        lines.append(f"- withdrawals: **{self.withdraw_usdc:.6f} USDC**")
        lines.append(f"- net cashflow: **{self.net_cashflow_usdc:.6f} USDC**")
        if self.ledger_type_counts:
            top_types = sorted(self.ledger_type_counts.items(), key=lambda kv: kv[1], reverse=True)[:10]
            lines.append("- ledger types:")
            for t, n in top_types:
                lines.append(f"- {t}: {n}")
        return "\n".join(lines)


def analyze_address(
    user: str,
    *,
    base_url: str,
    days: int,
    aggregate_by_time: bool = True,
    top_n: int = 10,
) -> AddressAnalysis:
    now_ms = int(time.time() * 1000)
    start_ms = now_ms - int(days) * 86400 * 1000
    end_ms = now_ms

    client = HyperliquidInfoClient(base_url=base_url)

    def _parse_positions(user_state: dict[str, Any]) -> tuple[float, float, float, list[dict[str, Any]]]:
        withdrawable = _f(user_state.get("withdrawable"))

        positions_raw = user_state.get("assetPositions") or []
        positions_by_coin: list[dict[str, Any]] = []
        total_pos_value = 0.0
        total_unrealized_pnl = 0.0

        if isinstance(positions_raw, list):
            for ap in positions_raw:
                if not isinstance(ap, dict):
                    continue
                pos = ap.get("position") if isinstance(ap.get("position"), dict) else {}
                coin = str(pos.get("coin", "")).strip()
                if not coin:
                    continue
                position_value = _f(pos.get("positionValue"))
                unrealized = _f(pos.get("unrealizedPnl"))
                margin_used = _f(pos.get("marginUsed"))
                lev = pos.get("leverage") if isinstance(pos.get("leverage"), dict) else {}

                total_pos_value += position_value
                total_unrealized_pnl += unrealized

                positions_by_coin.append(
                    {
                        "coin": coin,
                        "positionValueUsdc": position_value,
                        "marginUsedUsdc": margin_used,
                        "unrealizedPnlUsdc": unrealized,
                        "leverage": lev,
                        "entryPx": pos.get("entryPx"),
                        "liquidationPx": pos.get("liquidationPx"),
                        "szi": pos.get("szi"),
                        "returnOnEquity": pos.get("returnOnEquity"),
                    }
                )

        return withdrawable, total_pos_value, total_unrealized_pnl, positions_by_coin

    # 1) holdings (positions/margin) under two scopes

    # 1) holdings (positions/margin) under two scopes
    # - default: no dex param (crypto positions scope)
    user_state_default = client.user_state_no_dex(user)
    margin_summary_default = (
        user_state_default.get("marginSummary") if isinstance(user_state_default.get("marginSummary"), dict) else {}
    )
    cross_margin_summary_default = (
        user_state_default.get("crossMarginSummary")
        if isinstance(user_state_default.get("crossMarginSummary"), dict)
        else {}
    )
    withdrawable, total_pos_value, total_unrealized_pnl, positions_by_coin = _parse_positions(user_state_default)

    # - xyz: dex=xyz (HIP-3 xyz token scope)
    user_state_xyz = client.user_state(user, dex="xyz")
    margin_summary_xyz = (
        user_state_xyz.get("marginSummary") if isinstance(user_state_xyz.get("marginSummary"), dict) else {}
    )
    cross_margin_summary_xyz = (
        user_state_xyz.get("crossMarginSummary") if isinstance(user_state_xyz.get("crossMarginSummary"), dict) else {}
    )
    withdrawable_xyz, total_pos_value_xyz, total_unrealized_pnl_xyz, positions_by_coin_xyz = _parse_positions(user_state_xyz)

    # 2) fills (trade history)
    fills = client.user_fills_by_time(
        user,
        start_ms,
        end_ms,
        aggregate_by_time=bool(aggregate_by_time),
    )

    realized = 0.0
    fees = 0.0
    wins = 0
    losses = 0
    coin_counts: dict[str, int] = {}
    # pnl_history_daily uses SDK portfolio; we no longer aggregate daily from fills here

    for fill in fills:
        if not isinstance(fill, dict):
            continue
        realized += _f(fill.get("closedPnl"))
        fees += _f(fill.get("fee"))

        pnl = _f(fill.get("closedPnl"))
        if pnl > 0:
            wins += 1
        elif pnl < 0:
            losses += 1

        coin = str(fill.get("coin", "")).strip()
        if coin:
            coin_counts[coin] = coin_counts.get(coin, 0) + 1

        # pnl_history_daily now uses SDK portfolio (day.pnlHistory).
        # We keep fills-derived stats (win rate/fees/top coins), but no longer do daily aggregation.

    denom = wins + losses
    win_rate = (wins / denom) if denom > 0 else None
    top_coins = sorted(coin_counts.items(), key=lambda kv: kv[1], reverse=True)[: int(top_n)]

    # 3) pnl_history_daily: directly use SDK portfolio month.pnlHistory
    portfolio_data = client.portfolio(user)
    pnl_history_daily = get_pnl_history_daily_from_portfolio(
        portfolio_data=portfolio_data,
        start_ms=start_ms,
        end_ms=end_ms,
        days=int(days),
    )

    # 4) cash flow (non-funding ledger updates)
    ledger_updates = client.user_non_funding_ledger_updates(user, start_ms, end_ms)
    ledger_type_counts: Counter[str] = Counter()
    deposit_total = 0.0
    withdraw_total = 0.0

    def _extract_usdc_value(delta: dict[str, Any]) -> float:
        """
        Only count USDC in/out:
        - send/spotTransfer: require token == USDC
        - deposit/withdraw: if no token field, prefer usdc/usdcValue/amount
        """
        token = str(delta.get("token", "") or "").upper()
        has_usdc_field = any(k in delta for k in ("usdcValue", "usdc", "amount", "netWithdrawnUsd", "requestedUsd"))

        # send/spotTransfer usually includes token: ignore if token != USDC
        if token and token != "USDC":
            return 0.0

        if "usdcValue" in delta:
            return _f(delta.get("usdcValue"))
        if "usdc" in delta:
            return _f(delta.get("usdc"))
        if "netWithdrawnUsd" in delta:
            return _f(delta.get("netWithdrawnUsd"))
        if "requestedUsd" in delta:
            return _f(delta.get("requestedUsd"))
        if has_usdc_field:
            return _f(delta.get("amount"))
        return 0.0

    for upd in ledger_updates:
        if not isinstance(upd, dict):
            continue
        delta = upd.get("delta")
        if not isinstance(delta, dict):
            continue
        t = delta.get("type") or delta.get("deltaType") or "unknown"
        t = str(t)
        amount_usdc = _extract_usdc_value(delta)

        # deposit/withdraw may appear in some implementations; if present, accumulate directly
        if t in ("deposit", "withdraw"):
            if amount_usdc == 0.0:
                continue
            if t == "deposit":
                deposit_total += amount_usdc
            else:
                withdraw_total += amount_usdc
            ledger_type_counts[t] += 1
            continue

        # send/spotTransfer often includes user/destination and token/usdcValue for in/out direction
        if t in ("send", "spotTransfer"):
            src_user = str(delta.get("user", "") or "")
            destination = str(delta.get("destination", "") or "")
            amount = amount_usdc

            # self-transfer (destination == user and src_user == user): ignore
            if not src_user or not destination:
                continue
            if amount == 0.0:
                continue
            if src_user == user and destination != user:
                withdraw_total += amount
                ledger_type_counts[t] += 1
            elif destination == user and src_user != user:
                deposit_total += amount
                ledger_type_counts[t] += 1
            continue

    net_cashflow = deposit_total - withdraw_total

    return AddressAnalysis(
        user=user,
        analysis_start_ms=start_ms,
        analysis_end_ms=end_ms,
        days=int(days),
        withdrawable_usdc=withdrawable,
        margin_summary_default=margin_summary_default,
        cross_margin_summary_default=cross_margin_summary_default,
        total_position_value_usdc=total_pos_value,
        total_unrealized_pnl_usdc=total_unrealized_pnl,
        positions_by_coin=positions_by_coin,
        withdrawable_usdc_xyz=withdrawable_xyz,
        margin_summary_xyz=margin_summary_xyz,
        cross_margin_summary_xyz=cross_margin_summary_xyz,
        total_position_value_usdc_xyz=total_pos_value_xyz,
        total_unrealized_pnl_usdc_xyz=total_unrealized_pnl_xyz,
        positions_by_coin_xyz=positions_by_coin_xyz,
        fills_count=len(fills),
        realized_pnl_usdc=realized,
        fees_usdc=fees,
        win_rate=win_rate,
        top_coins=top_coins,
        pnl_history_daily=pnl_history_daily,
        deposit_usdc=deposit_total,
        withdraw_usdc=withdraw_total,
        net_cashflow_usdc=net_cashflow,
        ledger_type_counts=dict(ledger_type_counts),
    )


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(description="1m-trade Hyperliquid analyze (holdings/fills/PnL/cashflow)")
    p.add_argument("--user", required=True, help="address to analyze")
    p.add_argument("--out", help="markdown output path (default: stdout)")
    p.add_argument("--out-json", help="json output path (optional)")
    p.add_argument("--base-url", default=DEFAULT_HTTP_BASE_URL, help="Hyperliquid Info base URL")
    p.add_argument("--no-aggregate", action="store_true", help="disable aggregateByTime")
    p.add_argument("--days", type=int, default=30, help="analysis window in days (UTC, default 30)")
    p.add_argument("--top-n", type=int, default=10, help="top coins by fill count")
    return p


def run(args: argparse.Namespace) -> int:
    analysis = analyze_address(
        args.user,
        base_url=args.base_url,
        days=int(args.days),
        aggregate_by_time=not args.no_aggregate,
        top_n=int(args.top_n),
    )

    if args.out:
        with open(args.out, "w", encoding="utf-8") as f:
            f.write(analysis.to_markdown())
        print(f"wrote {args.out}")
    else:
        print(analysis.to_markdown())

    if args.out_json:
        with open(args.out_json, "w", encoding="utf-8") as f:
            json.dump(analysis.to_dict(), f, ensure_ascii=False, indent=2)
        print(f"wrote {args.out_json}")
    return 0


def main() -> int:
    args = build_parser().parse_args()
    return run(args)


if __name__ == "__main__":
    raise SystemExit(main())

