"""API endpoints for the web interface."""

import logging
import time

from fastapi import APIRouter, HTTPException, Query

logger = logging.getLogger(__name__)

from ..core.version import __version__
from ..data.database import (
    delete_operator,
    get_saved_operators,
    is_operator_saved,
    save_operator,
    update_operator_data,
)
from ..data.price import get_eth_price
from ..services.operator_service import OperatorService
from .identifiers import parse_operator_identifier

router = APIRouter()

# Per-operator cooldown for expensive save/refresh operations (seconds)
_REFRESH_COOLDOWN_SECONDS = 60
_last_refresh_time: dict[int, float] = {}


def _check_refresh_cooldown(operator_id: int) -> None:
    """Raise 429 if this operator was refreshed within the cooldown window."""
    last = _last_refresh_time.get(operator_id, 0.0)
    remaining = _REFRESH_COOLDOWN_SECONDS - (time.monotonic() - last)
    if remaining > 0:
        raise HTTPException(
            status_code=429,
            detail=f"Operator {operator_id} was refreshed recently. Retry in {int(remaining) + 1}s.",
        )


def _record_refresh(operator_id: int) -> None:
    """Record that operator_id was just refreshed."""
    _last_refresh_time[operator_id] = time.monotonic()


@router.get("/operator/{identifier}")
async def get_operator(
    identifier: str,
    detailed: bool = Query(False, description="Include validator status from beacon chain"),
    history: bool = Query(False, description="Include all historical distribution frames"),
    withdrawals: bool = Query(False, description="Include withdrawal/claim history"),
):
    """
    Get operator data by address or ID.

    - If identifier is numeric, treat as operator ID
    - If identifier starts with 0x, treat as Ethereum address
    - Add ?detailed=true to include validator status breakdown
    - Add ?history=true to include all historical distribution frames
    - Add ?withdrawals=true to include withdrawal/claim history
    """
    logger.info(f"Get operator: {identifier}, detailed={detailed}, history={history}, withdrawals={withdrawals}")
    service = OperatorService()

    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
        rewards = await service.get_operator_by_id(operator_id, detailed or history, history, withdrawals)
    else:
        rewards = await service.get_operator_by_address(parsed_identifier, detailed or history, history, withdrawals)

    if rewards is None:
        raise HTTPException(status_code=404, detail="Operator not found")

    result = {
        "version": __version__,
        "operator_id": rewards.node_operator_id,
        "manager_address": rewards.manager_address,
        "reward_address": rewards.reward_address,
        "curve_id": rewards.curve_id,
        "operator_type": rewards.operator_type,
        "rewards": {
            "current_bond_eth": str(rewards.current_bond_eth),
            "required_bond_eth": str(rewards.required_bond_eth),
            "excess_bond_eth": str(rewards.excess_bond_eth),
            "cumulative_rewards_shares": rewards.cumulative_rewards_shares,
            "cumulative_rewards_eth": str(rewards.cumulative_rewards_eth),
            "distributed_shares": rewards.distributed_shares,
            "distributed_eth": str(rewards.distributed_eth),
            "unclaimed_shares": rewards.unclaimed_shares,
            "unclaimed_eth": str(rewards.unclaimed_eth),
            "total_claimable_eth": str(rewards.total_claimable_eth),
        },
        "validators": {
            "total": rewards.total_validators,
            "active": rewards.active_validators,
            "exited": rewards.exited_validators,
        },
    }

    # Fetch active_since for basic (non-detailed) requests
    # For detailed requests, it's already included in rewards.active_since
    if not detailed and rewards.total_validators > 0:
        active_since = await service.get_operator_active_since(rewards.node_operator_id)
        if active_since:
            result["active_since"] = active_since.isoformat()

    # Add beacon chain validator details if available
    if rewards.validators_by_status:
        result["validators"]["by_status"] = rewards.validators_by_status

    if rewards.avg_effectiveness is not None:
        result["performance"] = {
            "avg_effectiveness": round(rewards.avg_effectiveness, 2),
        }

    if detailed and rewards.validator_details:
        result["validator_details"] = [v.to_dict() for v in rewards.validator_details]

    # Add APY metrics if available
    if rewards.apy:
        # Use actual excess bond for lifetime values (estimates for previous/current)
        lifetime_bond = float(rewards.excess_bond_eth)
        lifetime_net_total = (rewards.apy.lifetime_distribution_eth or 0) + lifetime_bond
        result["apy"] = {
            "previous_distribution_eth": rewards.apy.previous_distribution_eth,
            "previous_distribution_apy": rewards.apy.previous_distribution_apy,
            "previous_net_apy": rewards.apy.previous_net_apy,
            "previous_bond_eth": rewards.apy.previous_bond_eth,
            "previous_bond_apr": rewards.apy.previous_bond_apr,
            "previous_net_total_eth": rewards.apy.previous_net_total_eth,
            "current_distribution_eth": rewards.apy.current_distribution_eth,
            "current_distribution_apy": rewards.apy.current_distribution_apy,
            "current_bond_eth": rewards.apy.current_bond_eth,
            "current_bond_apr": rewards.apy.current_bond_apr,
            "current_net_total_eth": rewards.apy.current_net_total_eth,
            "lifetime_distribution_eth": rewards.apy.lifetime_distribution_eth,
            "lifetime_bond_eth": lifetime_bond,  # Actual excess bond, not estimate
            "lifetime_net_total_eth": lifetime_net_total,  # Matches Total Claimable
            # Accurate lifetime APY (per-frame bond calculation when available)
            "lifetime_reward_apy": rewards.apy.lifetime_reward_apy,
            "lifetime_bond_apy": rewards.apy.lifetime_bond_apy,
            "lifetime_net_apy": rewards.apy.lifetime_net_apy,
            "next_distribution_date": rewards.apy.next_distribution_date,
            "next_distribution_est_eth": rewards.apy.next_distribution_est_eth,
            "historical_reward_apy_28d": rewards.apy.historical_reward_apy_28d,
            "historical_reward_apy_ltd": rewards.apy.historical_reward_apy_ltd,
            "bond_apy": rewards.apy.bond_apy,
            "net_apy_28d": rewards.apy.net_apy_28d,
            "net_apy_ltd": rewards.apy.net_apy_ltd,
            "uses_historical_apr": rewards.apy.uses_historical_apr,
        }
        # Add frames if available (from history=true)
        if rewards.apy.frames:
            result["apy"]["frames"] = [
                {
                    "frame_number": f.frame_number,
                    "start_date": f.start_date,
                    "end_date": f.end_date,
                    "rewards_eth": f.rewards_eth,
                    "rewards_shares": f.rewards_shares,
                    "duration_days": f.duration_days,
                    "validator_count": f.validator_count,
                    "apy": f.apy,
                    "bond_apy": f.bond_apy,
                    "net_apy": f.net_apy,
                }
                for f in rewards.apy.frames
            ]
        # Add capital efficiency if available
        if rewards.apy.capital_efficiency:
            ce = rewards.apy.capital_efficiency
            result["apy"]["capital_efficiency"] = {
                "total_csm_return_eth": ce.total_csm_return_eth,
                "total_capital_deployed_eth": ce.total_capital_deployed_eth,
                "csm_annualized_return_pct": ce.csm_annualized_return_pct,
                "steth_benchmark_return_pct": ce.steth_benchmark_return_pct,
                "csm_advantage_ratio": ce.csm_advantage_ratio,
                "first_deposit_date": ce.first_deposit_date,
                "days_operating": ce.days_operating,
                "xirr_pct": ce.xirr_pct,
            }

    # Add withdrawal history if withdrawals=true (already fetched during data gathering)
    if withdrawals and rewards.withdrawals:
        result["withdrawals"] = [
            {
                "block_number": w.block_number,
                "timestamp": w.timestamp,
                "shares": w.shares,
                "eth_value": w.eth_value,
                "tx_hash": w.tx_hash,
                "withdrawal_type": w.withdrawal_type,
                "request_id": w.request_id,
                "status": w.status,
                "claimed_eth": w.claimed_eth,
                "claim_tx_hash": w.claim_tx_hash,
                "claim_timestamp": w.claim_timestamp,
            }
            for w in rewards.withdrawals
        ]

        # Add summary for pending unstETH requests
        pending_unsteth = [
            w for w in rewards.withdrawals
            if w.withdrawal_type == "unstETH"
            and w.status in ("pending", "finalized")
        ]
        if pending_unsteth:
            result["pending_unsteth"] = {
                "count": len(pending_unsteth),
                "ready_to_claim": sum(1 for w in pending_unsteth if w.status == "finalized"),
                "total_steth_value": sum(w.eth_value for w in pending_unsteth),
            }

    # Add active_since if available
    if rewards.active_since:
        result["active_since"] = rewards.active_since.isoformat()

    # Add health status if available
    if rewards.health:
        result["health"] = {
            "bond_healthy": rewards.health.bond_healthy,
            "bond_deficit_eth": str(rewards.health.bond_deficit_eth),
            "stuck_validators_count": rewards.health.stuck_validators_count,
            "slashed_validators_count": rewards.health.slashed_validators_count,
            "validators_at_risk_count": rewards.health.validators_at_risk_count,
            "strikes": {
                "total_validators_with_strikes": rewards.health.strikes.total_validators_with_strikes,
                "validators_at_risk": rewards.health.strikes.validators_at_risk,
                "validators_near_ejection": rewards.health.strikes.validators_near_ejection,
                "total_strikes": rewards.health.strikes.total_strikes,
                "max_strikes": rewards.health.strikes.max_strikes,
                "strike_threshold": rewards.health.strikes.strike_threshold,
            },
            "has_issues": rewards.health.has_issues,
        }

    return result


@router.get("/operators")
async def list_operators():
    """List all operators with rewards in the current tree."""
    service = OperatorService()
    operator_ids = await service.get_all_operators_with_rewards()
    return {"count": len(operator_ids), "operator_ids": operator_ids}


@router.get("/operator/{identifier}/strikes")
async def get_operator_strikes(identifier: str):
    """Get detailed strikes for an operator's validators."""
    service = OperatorService()

    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
    else:
        operator_id = await service.onchain.find_operator_by_address(parsed_identifier)
        if operator_id is None:
            raise HTTPException(status_code=404, detail="Operator not found")

    # Get curve_id to determine strike threshold
    curve_id = await service.onchain.get_bond_curve_id(operator_id)
    strikes = await service.get_operator_strikes(operator_id, curve_id)

    # Fetch frame dates for tooltip display
    frame_dates = await service.get_recent_frame_dates(6)

    # Get the threshold for this operator type
    strike_threshold = strikes[0].strike_threshold if strikes else 3

    return {
        "operator_id": operator_id,
        "strike_threshold": strike_threshold,
        "frame_dates": frame_dates,
        "validators": [
            {
                "pubkey": s.pubkey,
                "strike_count": s.strike_count,
                "strike_threshold": s.strike_threshold,
                "strikes": s.strikes,
                "at_ejection_risk": s.at_ejection_risk,
            }
            for s in strikes
        ],
    }


@router.get("/saved-operators")
async def list_saved_operators():
    """Get all saved operators with their cached data."""
    logger.info("Loading saved operators from database")
    try:
        operators = await get_saved_operators()
        # Log what data each operator has
        for op in operators:
            op_id = op.get("operator_id", "?")
            frames = len(op.get("apy", {}).get("frames", []))
            withdrawals = len(op.get("withdrawals", []))
            logger.info(f"  Operator {op_id}: {frames} frames, {withdrawals} withdrawals in cache")
        logger.info(f"Loaded {len(operators)} saved operators")
        return {"operators": operators}
    except Exception as e:
        logger.error(f"Error loading saved operators: {e}", exc_info=True)
        return {"operators": [], "error": str(e)}


def _build_operator_data_dict(rewards) -> dict:
    """Build the serializable data dict from an OperatorRewards object.

    Used by both save and refresh endpoints to ensure consistent output.
    """
    data = {
        "operator_id": rewards.node_operator_id,
        "manager_address": rewards.manager_address,
        "reward_address": rewards.reward_address,
        "curve_id": rewards.curve_id,
        "operator_type": rewards.operator_type,
        "rewards": {
            "current_bond_eth": str(rewards.current_bond_eth),
            "required_bond_eth": str(rewards.required_bond_eth),
            "excess_bond_eth": str(rewards.excess_bond_eth),
            "cumulative_rewards_shares": rewards.cumulative_rewards_shares,
            "cumulative_rewards_eth": str(rewards.cumulative_rewards_eth),
            "distributed_shares": rewards.distributed_shares,
            "distributed_eth": str(rewards.distributed_eth),
            "unclaimed_shares": rewards.unclaimed_shares,
            "unclaimed_eth": str(rewards.unclaimed_eth),
            "total_claimable_eth": str(rewards.total_claimable_eth),
        },
        "validators": {
            "total": rewards.total_validators,
            "active": rewards.active_validators,
            "exited": rewards.exited_validators,
        },
    }

    if rewards.validators_by_status:
        data["validators"]["by_status"] = rewards.validators_by_status

    if rewards.avg_effectiveness is not None:
        data["performance"] = {
            "avg_effectiveness": round(rewards.avg_effectiveness, 2),
        }

    if rewards.active_since:
        data["active_since"] = rewards.active_since.isoformat()

    if rewards.apy:
        data["apy"] = {
            "historical_reward_apy_28d": rewards.apy.historical_reward_apy_28d,
            "historical_reward_apy_ltd": rewards.apy.historical_reward_apy_ltd,
            "bond_apy": rewards.apy.bond_apy,
            "net_apy_28d": rewards.apy.net_apy_28d,
            "net_apy_ltd": rewards.apy.net_apy_ltd,
            "next_distribution_date": rewards.apy.next_distribution_date,
            "next_distribution_est_eth": rewards.apy.next_distribution_est_eth,
            "lifetime_reward_apy": rewards.apy.lifetime_reward_apy,
            "lifetime_bond_apy": rewards.apy.lifetime_bond_apy,
            "lifetime_net_apy": rewards.apy.lifetime_net_apy,
        }
        if rewards.apy.frames:
            data["apy"]["frames"] = [
                {
                    "frame_number": f.frame_number,
                    "start_date": f.start_date,
                    "end_date": f.end_date,
                    "rewards_eth": f.rewards_eth,
                    "rewards_shares": f.rewards_shares,
                    "duration_days": f.duration_days,
                    "validator_count": f.validator_count,
                    "apy": f.apy,
                    "bond_apy": f.bond_apy,
                    "net_apy": f.net_apy,
                }
                for f in rewards.apy.frames
            ]
        if rewards.apy.capital_efficiency:
            ce = rewards.apy.capital_efficiency
            data["apy"]["capital_efficiency"] = {
                "total_csm_return_eth": ce.total_csm_return_eth,
                "total_capital_deployed_eth": ce.total_capital_deployed_eth,
                "csm_annualized_return_pct": ce.csm_annualized_return_pct,
                "steth_benchmark_return_pct": ce.steth_benchmark_return_pct,
                "csm_advantage_ratio": ce.csm_advantage_ratio,
                "first_deposit_date": ce.first_deposit_date,
                "days_operating": ce.days_operating,
                "xirr_pct": ce.xirr_pct,
            }

    if rewards.health:
        data["health"] = {
            "bond_healthy": rewards.health.bond_healthy,
            "bond_deficit_eth": str(rewards.health.bond_deficit_eth),
            "stuck_validators_count": rewards.health.stuck_validators_count,
            "slashed_validators_count": rewards.health.slashed_validators_count,
            "validators_at_risk_count": rewards.health.validators_at_risk_count,
            "strikes": {
                "total_validators_with_strikes": rewards.health.strikes.total_validators_with_strikes,
                "validators_at_risk": rewards.health.strikes.validators_at_risk,
                "validators_near_ejection": rewards.health.strikes.validators_near_ejection,
                "total_strikes": rewards.health.strikes.total_strikes,
                "max_strikes": rewards.health.strikes.max_strikes,
                "strike_threshold": rewards.health.strikes.strike_threshold,
            },
            "has_issues": rewards.health.has_issues,
        }

    if rewards.withdrawals:
        data["withdrawals"] = [
            {
                "block_number": w.block_number,
                "timestamp": w.timestamp,
                "shares": w.shares,
                "eth_value": w.eth_value,
                "tx_hash": w.tx_hash,
                "withdrawal_type": w.withdrawal_type,
                "request_id": w.request_id,
                "status": w.status,
                "claimed_eth": w.claimed_eth,
                "claim_tx_hash": w.claim_tx_hash,
                "claim_timestamp": w.claim_timestamp,
            }
            for w in rewards.withdrawals
        ]

    return data


@router.post("/operator/{identifier}/save")
async def save_operator_endpoint(identifier: str):
    """Save an operator to the follow list.

    Fetches current operator data (including history and withdrawals) and stores it in the database.
    """
    logger.info(f"Saving operator: {identifier}")
    service = OperatorService()

    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
    else:
        operator_id = await service.onchain.find_operator_by_address(parsed_identifier)
        if operator_id is None:
            raise HTTPException(status_code=404, detail="Operator not found")

    _check_refresh_cooldown(operator_id)

    # Fetch current operator data with history and withdrawals
    rewards = await service.get_operator_by_id(
        operator_id,
        include_validators=True,
        include_history=True,
        include_withdrawals=True,
    )
    if rewards is None:
        raise HTTPException(status_code=404, detail="Operator not found")

    data = _build_operator_data_dict(rewards)

    # Save to database
    frames_count = len(data.get("apy", {}).get("frames", []))
    withdrawals_count = len(data.get("withdrawals", []))
    logger.info(f"Saving operator {operator_id}: {frames_count} frames, {withdrawals_count} withdrawals")
    _record_refresh(operator_id)
    await save_operator(operator_id, data)

    return {"status": "saved", "operator_id": operator_id}


@router.delete("/operator/{identifier}/save")
async def unsave_operator_endpoint(identifier: str):
    """Remove an operator from the follow list."""
    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
    else:
        service = OperatorService()
        operator_id = await service.onchain.find_operator_by_address(parsed_identifier)
        if operator_id is None:
            raise HTTPException(status_code=404, detail="Operator not found")

    deleted = await delete_operator(operator_id)
    if not deleted:
        raise HTTPException(status_code=404, detail="Operator not in saved list")

    return {"status": "removed", "operator_id": operator_id}


@router.get("/operator/{identifier}/saved")
async def check_operator_saved(identifier: str):
    """Check if an operator is in the saved list."""
    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
    else:
        service = OperatorService()
        operator_id = await service.onchain.find_operator_by_address(parsed_identifier)
        if operator_id is None:
            return {"saved": False}

    saved = await is_operator_saved(operator_id)
    return {"saved": saved, "operator_id": operator_id}


@router.post("/operator/{identifier}/refresh")
async def refresh_operator_endpoint(identifier: str):
    """Refresh the cached data for a saved operator.

    Fetches fresh data (including history and withdrawals) from APIs and updates the database.
    """
    logger.info(f"Refreshing operator: {identifier}")
    service = OperatorService()

    id_type, parsed_identifier = parse_operator_identifier(identifier)
    if id_type == "id":
        operator_id = parsed_identifier
    else:
        operator_id = await service.onchain.find_operator_by_address(parsed_identifier)
        if operator_id is None:
            raise HTTPException(status_code=404, detail="Operator not found")

    # Check if operator is saved
    if not await is_operator_saved(operator_id):
        raise HTTPException(status_code=404, detail="Operator not in saved list")

    _check_refresh_cooldown(operator_id)

    # Fetch fresh data with history and withdrawals
    rewards = await service.get_operator_by_id(
        operator_id,
        include_validators=True,
        include_history=True,
        include_withdrawals=True,
    )
    if rewards is None:
        raise HTTPException(status_code=404, detail="Operator not found")

    data = _build_operator_data_dict(rewards)

    # Update in database
    frames_count = len(data.get("apy", {}).get("frames", []))
    withdrawals_count = len(data.get("withdrawals", []))
    logger.info(f"Refreshing operator {operator_id}: {frames_count} frames, {withdrawals_count} withdrawals")
    _record_refresh(operator_id)
    await update_operator_data(operator_id, data)

    return {"status": "refreshed", "operator_id": operator_id, "data": data}


@router.get("/health")
async def health_check():
    """Health check endpoint."""
    logger.debug("Health check called")
    return {"status": "healthy"}


@router.get("/price/eth")
async def get_eth_price_endpoint():
    """Get current ETH price in USD from CoinGecko."""
    price = await get_eth_price()
    if price is None:
        return {"price": None, "error": "Failed to fetch price"}
    return {"price": price, "currency": "USD"}
