"""Operator approval workflow for live trading mode.

When live trading is enabled and operator approval is required,
orders are held in a pending state until an operator explicitly
approves or rejects them. Expired approvals are treated as rejections.

Requirements: 8.2
Design: Section 4.8 - Risk Engine (operator approval rules)
"""
from __future__ import annotations

import json
import logging
import uuid
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Any

import asyncpg

from services.risk.engine import (
    PortfolioRiskConfig,
    TradingMode,
)

logger = logging.getLogger("operator_approval")


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


class ApprovalStatus(str, Enum):
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    EXPIRED = "expired"


# ---------------------------------------------------------------------------
# Core logic: does this order need approval?
# ---------------------------------------------------------------------------


def requires_approval(
    config: PortfolioRiskConfig,
    trading_mode: TradingMode | None = None,
) -> bool:
    """Determine whether an order requires operator approval.

    Paper orders are auto-approved when auto_approve_paper is True.
    Live orders require approval when require_approval_for_live is True.
    Disabled mode always returns False (orders are blocked upstream).
    """
    mode = trading_mode or config.trading_mode

    if mode == TradingMode.DISABLED:
        return False

    if mode == TradingMode.PAPER:
        return not config.operator_approval.auto_approve_paper

    # Live mode
    return config.operator_approval.require_approval_for_live


def compute_expiry(
    config: PortfolioRiskConfig,
    now: datetime | None = None,
) -> datetime:
    """Compute the expiry timestamp for a new approval request."""
    now = now or datetime.now(timezone.utc)
    return now + timedelta(minutes=config.operator_approval.approval_timeout_minutes)


# ---------------------------------------------------------------------------
# Approval request model (in-memory representation)
# ---------------------------------------------------------------------------


class ApprovalRequest:
    """Represents a pending operator approval request."""

    def __init__(
        self,
        approval_id: str | None = None,
        order_job: dict[str, Any] | None = None,
        recommendation_id: str | None = None,
        ticker: str = "",
        side: str = "buy",
        quantity: float = 0.0,
        estimated_value: float = 0.0,
        risk_evaluation_id: str | None = None,
        status: ApprovalStatus = ApprovalStatus.PENDING,
        requested_by: str = "system",
        reviewed_by: str | None = None,
        review_note: str | None = None,
        expires_at: datetime | None = None,
        requested_at: datetime | None = None,
        reviewed_at: datetime | None = None,
    ) -> None:
        self.approval_id = approval_id or str(uuid.uuid4())
        self.order_job = order_job or {}
        self.recommendation_id = recommendation_id
        self.ticker = ticker
        self.side = side
        self.quantity = quantity
        self.estimated_value = estimated_value
        self.risk_evaluation_id = risk_evaluation_id
        self.status = status
        self.requested_by = requested_by
        self.reviewed_by = reviewed_by
        self.review_note = review_note
        self.expires_at = expires_at or (datetime.now(timezone.utc) + timedelta(minutes=30))
        self.requested_at = requested_at or datetime.now(timezone.utc)
        self.reviewed_at = reviewed_at

    @property
    def is_pending(self) -> bool:
        return self.status == ApprovalStatus.PENDING

    @property
    def is_expired(self) -> bool:
        if self.status == ApprovalStatus.EXPIRED:
            return True
        if self.status == ApprovalStatus.PENDING:
            return datetime.now(timezone.utc) >= self.expires_at
        return False

    def to_dict(self) -> dict[str, Any]:
        return {
            "approval_id": self.approval_id,
            "recommendation_id": self.recommendation_id,
            "ticker": self.ticker,
            "side": self.side,
            "quantity": self.quantity,
            "estimated_value": self.estimated_value,
            "risk_evaluation_id": self.risk_evaluation_id,
            "status": self.status.value,
            "requested_by": self.requested_by,
            "reviewed_by": self.reviewed_by,
            "review_note": self.review_note,
            "expires_at": self.expires_at.isoformat() if self.expires_at else None,
            "requested_at": self.requested_at.isoformat() if self.requested_at else None,
            "reviewed_at": self.reviewed_at.isoformat() if self.reviewed_at else None,
        }


# ---------------------------------------------------------------------------
# DB persistence
# ---------------------------------------------------------------------------

_INSERT_APPROVAL = """
INSERT INTO operator_approvals (
    id, order_job, recommendation_id, ticker, side, quantity,
    estimated_value, status, risk_evaluation_id, requested_by,
    expires_at, requested_at
) VALUES (
    $1::uuid, $2::jsonb, $3, $4, $5, $6,
    $7, $8, $9, $10,
    $11, $12
)
"""

_UPDATE_APPROVAL_STATUS = """
UPDATE operator_approvals
SET status = $2, reviewed_by = $3, review_note = $4, reviewed_at = $5, updated_at = NOW()
WHERE id = $1::uuid AND status = 'pending'
RETURNING id, status
"""

_EXPIRE_STALE_APPROVALS = """
UPDATE operator_approvals
SET status = 'expired', updated_at = NOW()
WHERE status = 'pending' AND expires_at <= $1
RETURNING id, ticker
"""

_FETCH_PENDING_APPROVALS = """
SELECT id, order_job, recommendation_id, ticker, side, quantity,
       estimated_value, status, risk_evaluation_id, requested_by,
       reviewed_by, review_note, expires_at, requested_at, reviewed_at
FROM operator_approvals
WHERE status = 'pending'
ORDER BY requested_at ASC
"""

_FETCH_APPROVAL_BY_ID = """
SELECT id, order_job, recommendation_id, ticker, side, quantity,
       estimated_value, status, risk_evaluation_id, requested_by,
       reviewed_by, review_note, expires_at, requested_at, reviewed_at
FROM operator_approvals
WHERE id = $1::uuid
"""


def _row_to_request(row: Any) -> ApprovalRequest:
    """Convert a DB row to an ApprovalRequest."""
    order_job = row["order_job"]
    if isinstance(order_job, str):
        order_job = json.loads(order_job)
    return ApprovalRequest(
        approval_id=str(row["id"]),
        order_job=order_job,
        recommendation_id=str(row["recommendation_id"]) if row["recommendation_id"] else None,
        ticker=row["ticker"],
        side=row["side"],
        quantity=float(row["quantity"]),
        estimated_value=float(row["estimated_value"]),
        risk_evaluation_id=str(row["risk_evaluation_id"]) if row.get("risk_evaluation_id") else None,
        status=ApprovalStatus(row["status"]),
        requested_by=row["requested_by"],
        reviewed_by=row["reviewed_by"],
        review_note=row["review_note"],
        expires_at=row["expires_at"],
        requested_at=row["requested_at"],
        reviewed_at=row["reviewed_at"],
    )


async def create_approval_request(
    pool: asyncpg.Pool,
    request: ApprovalRequest,
) -> str:
    """Persist a new approval request. Returns the approval ID."""
    await pool.execute(
        _INSERT_APPROVAL,
        request.approval_id,
        json.dumps(request.order_job, default=str),
        request.recommendation_id,
        request.ticker,
        request.side,
        request.quantity,
        request.estimated_value,
        request.status.value,
        request.risk_evaluation_id,
        request.requested_by,
        request.expires_at,
        request.requested_at,
    )
    return request.approval_id


async def review_approval(
    pool: asyncpg.Pool,
    approval_id: str,
    approved: bool,
    reviewed_by: str = "operator",
    review_note: str = "",
) -> ApprovalStatus | None:
    """Approve or reject a pending approval request.

    Returns the new status, or None if the approval was not found
    or was no longer pending (already expired/reviewed).
    """
    now = datetime.now(timezone.utc)
    new_status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED

    row = await pool.fetchrow(
        _UPDATE_APPROVAL_STATUS,
        approval_id,
        new_status.value,
        reviewed_by,
        review_note,
        now,
    )
    if row:
        return ApprovalStatus(row["status"])
    return None


async def expire_stale_approvals(
    pool: asyncpg.Pool,
    now: datetime | None = None,
) -> list[dict[str, str]]:
    """Mark all expired pending approvals. Returns list of expired items."""
    now = now or datetime.now(timezone.utc)
    rows = await pool.fetch(_EXPIRE_STALE_APPROVALS, now)
    return [{"id": str(r["id"]), "ticker": r["ticker"]} for r in rows]


async def get_pending_approvals(
    pool: asyncpg.Pool,
) -> list[ApprovalRequest]:
    """Fetch all pending approval requests, oldest first."""
    rows = await pool.fetch(_FETCH_PENDING_APPROVALS)
    return [_row_to_request(r) for r in rows]


async def get_approval_by_id(
    pool: asyncpg.Pool,
    approval_id: str,
) -> ApprovalRequest | None:
    """Fetch a single approval request by ID."""
    row = await pool.fetchrow(_FETCH_APPROVAL_BY_ID, approval_id)
    if row:
        return _row_to_request(row)
    return None
