"""
AwoooP Approval Token — HS256 簽核令牌 + Multi-sig + Suggest Mode
==================================================================
AwoooP Phase 8: ADR-116 Gate 5 approval flow
2026-05-04 ogt + Claude Sonnet 4.6

功能：
  1. HS256 Approval Token（自製，不依賴 PyJWT）：
     - issue_approval_token() → signed token（3 段 base64url）
     - verify_approval_token() → payload（含 jti/exp/sub/approver）
     - jti 存 Redis NX（TTL = exp - now）防 token replay
     - TTL = 15 分鐘（APPROVAL_TOKEN_TTL = 900s）

  2. Multi-sig quorum：
     - record_approval() → 驗 token + NX jti + SADD approver_id → 目前簽核數
     - check_approval_quorum(required=1) → bool | raise QuorumNotMetError
     - Redis Set TTL = 1h

  3. Suggest Mode（AWOOOP_SUGGEST_MODE feature flag）：
     - is_suggest_mode_enabled() → bool
     - build_suggest_action(action_type, target) → SuggestedAction（dry-run）
     - 支援 3 個 SRE flow：rollback / scale / restart

Redis key 前綴（與 legacy multi_sig_redis.py 不衝突）：
  awooop_appr:jti:{jti}       — NX token replay 防護
  awooop_appr:sigs:{project_id}:{run_id}:{tool_name}  — 簽核人 Set

錯誤碼：
  E-APPR-001  token 無效或已過期
  E-APPR-002  jti 已使用（replay attack）
  E-APPR-003  quorum 未達
  E-APPR-004  approver 重複簽核
"""

from __future__ import annotations

import base64
import hashlib
import hmac as _hmac_module
import json
import os
import time
import uuid
from dataclasses import dataclass, field
from typing import Any

import structlog

from src.core.redis_client import get_redis

logger = structlog.get_logger(__name__)

# ─────────────────────────────────────────────────────────────────────────────
# 常數
# ─────────────────────────────────────────────────────────────────────────────

APPROVAL_TOKEN_TTL = 900           # 15 分鐘
_JTI_KEY_PREFIX = "awooop_appr:jti:"
_SIG_SET_PREFIX = "awooop_appr:sigs:"
_SIG_TTL_SECONDS = 3600            # 簽核 Set 1h TTL
_SUGGEST_MODE_ENV = "AWOOOP_SUGGEST_MODE"


# ─────────────────────────────────────────────────────────────────────────────
# 錯誤定義
# ─────────────────────────────────────────────────────────────────────────────

class InvalidApprovalTokenError(Exception):
    error_code = "E-APPR-001"

class TokenReplayError(Exception):
    error_code = "E-APPR-002"

class QuorumNotMetError(Exception):
    error_code = "E-APPR-003"

class DuplicateApproverError(Exception):
    error_code = "E-APPR-004"


# ─────────────────────────────────────────────────────────────────────────────
# HS256 Token 實作
# ─────────────────────────────────────────────────────────────────────────────

def _b64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b"=").decode()


def _b64url_decode(s: str) -> bytes:
    padding = 4 - len(s) % 4
    if padding != 4:
        s += "=" * padding
    return base64.urlsafe_b64decode(s)


def _get_hmac_key() -> bytes:
    try:
        from src.core.config import settings
        key = getattr(settings, "APPROVAL_HMAC_KEY", None) or ""
    except Exception:
        key = ""
    key = key or os.environ.get("APPROVAL_HMAC_KEY", "")
    if not key:
        logger.warning("approval_hmac_key_not_set_using_dev_fallback")
        key = "dev-awooop-approval-hmac-fallback"
    return key.encode()


def issue_approval_token(
    *,
    project_id: str,
    run_id: str,
    tool_name: str,
    approver_id: str,
    ttl_seconds: int = APPROVAL_TOKEN_TTL,
) -> str:
    """
    產生 HS256 Approval Token。

    payload：
      jti = uuid4().hex（唯一 token ID，用於 Redis NX 防 replay）
      iss = "awooop-approval"
      sub = "{project_id}:{run_id}:{tool_name}"
      approver = approver_id
      iat / exp
    """
    now = int(time.time())
    jti = uuid.uuid4().hex

    header = {"alg": "HS256", "typ": "JWT"}
    payload = {
        "jti": jti,
        "iss": "awooop-approval",
        "sub": f"{project_id}:{run_id}:{tool_name}",
        "approver": approver_id,
        "iat": now,
        "exp": now + ttl_seconds,
    }

    h_b64 = _b64url_encode(json.dumps(header, separators=(",", ":")).encode())
    p_b64 = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
    signing_input = f"{h_b64}.{p_b64}"

    sig = _hmac_module.new(
        _get_hmac_key(),
        signing_input.encode(),
        hashlib.sha256,
    ).digest()
    return f"{signing_input}.{_b64url_encode(sig)}"


def verify_approval_token(token: str) -> dict[str, Any]:
    """
    驗證 HS256 token，回傳 payload。

    Raises:
        InvalidApprovalTokenError: 簽名無效/過期/格式錯誤
    """
    try:
        parts = token.split(".")
        if len(parts) != 3:
            raise InvalidApprovalTokenError("token 非 3 段格式")

        h_b64, p_b64, sig_b64 = parts
        signing_input = f"{h_b64}.{p_b64}"

        expected_sig = _hmac_module.new(
            _get_hmac_key(),
            signing_input.encode(),
            hashlib.sha256,
        ).digest()

        if not _hmac_module.compare_digest(sig_b64, _b64url_encode(expected_sig)):
            raise InvalidApprovalTokenError("token 簽名無效")

        payload = json.loads(_b64url_decode(p_b64))

        if int(time.time()) > payload.get("exp", 0):
            raise InvalidApprovalTokenError("token 已過期")

        return payload

    except InvalidApprovalTokenError:
        raise
    except Exception as exc:
        raise InvalidApprovalTokenError(f"token 解析失敗: {exc}") from exc


# ─────────────────────────────────────────────────────────────────────────────
# Multi-sig Redis approval
# ─────────────────────────────────────────────────────────────────────────────

async def record_approval(
    *,
    project_id: str,
    run_id: str,
    tool_name: str,
    approver_id: str,
    token: str,
) -> int:
    """
    記錄一筆簽核。步驟：
    1. verify_approval_token（HS256 + exp）
    2. sub 匹配驗證
    3. Redis NX jti（防 replay）
    4. Redis SADD approver_id（防重複）
    5. 回傳目前簽核數

    Raises:
        InvalidApprovalTokenError, TokenReplayError, DuplicateApproverError
    """
    payload = verify_approval_token(token)

    expected_sub = f"{project_id}:{run_id}:{tool_name}"
    if payload.get("sub") != expected_sub:
        raise InvalidApprovalTokenError(
            f"token sub 不符（期望 '{expected_sub}'，實際 '{payload.get('sub')}'）"
        )

    jti = payload["jti"]
    exp = payload["exp"]

    try:
        redis = get_redis()

        # jti NX
        jti_key = f"{_JTI_KEY_PREFIX}{jti}"
        ttl_remaining = max(exp - int(time.time()), 1)
        ok = await redis.set(jti_key, "1", nx=True, ex=ttl_remaining)
        if not ok:
            raise TokenReplayError(f"jti={jti!r} 已使用")

        # SADD approver
        sig_key = f"{_SIG_SET_PREFIX}{project_id}:{run_id}:{tool_name}"
        added = await redis.sadd(sig_key, approver_id)
        if added == 0:
            raise DuplicateApproverError(f"approver '{approver_id}' 已簽核")

        await redis.expire(sig_key, _SIG_TTL_SECONDS)
        count = int(await redis.scard(sig_key))

        logger.info(
            "awooop_approval_recorded",
            project_id=project_id,
            run_id=run_id,
            tool_name=tool_name,
            approver_id=approver_id,
            count=count,
        )
        return count

    except (InvalidApprovalTokenError, TokenReplayError, DuplicateApproverError):
        raise
    except Exception as exc:
        logger.exception("awooop_approval_redis_error", error=str(exc))
        raise InvalidApprovalTokenError(f"Redis 錯誤: {exc}") from exc


async def check_approval_quorum(
    *,
    project_id: str,
    run_id: str,
    tool_name: str,
    required_count: int = 1,
) -> bool:
    """
    檢查 quorum。Raises QuorumNotMetError if 不足。
    """
    try:
        redis = get_redis()
        sig_key = f"{_SIG_SET_PREFIX}{project_id}:{run_id}:{tool_name}"
        count = int(await redis.scard(sig_key))

        if count < required_count:
            raise QuorumNotMetError(f"簽核數不足（{count}/{required_count}）")
        return True

    except QuorumNotMetError:
        raise
    except Exception as exc:
        raise QuorumNotMetError(f"Redis 查詢失敗: {exc}") from exc


# ─────────────────────────────────────────────────────────────────────────────
# Suggest Mode
# ─────────────────────────────────────────────────────────────────────────────

@dataclass
class SuggestedAction:
    """Suggest mode dry-run 結果（不真正執行）"""
    action_type: str           # 'rollback' | 'scale' | 'restart'
    target: str
    suggested_command: str
    rollback_evidence: dict[str, Any] = field(default_factory=dict)
    dry_run: bool = True
    approval_required: bool = True


def is_suggest_mode_enabled() -> bool:
    return os.environ.get(_SUGGEST_MODE_ENV, "").lower() in ("true", "1", "yes")


async def build_suggest_action(
    action_type: str,
    *,
    target: str,
    run_id: str,
    project_id: str,
) -> SuggestedAction:
    """
    Suggest mode：返回 dry-run 建議，不執行真實操作。
    支援 rollback / scale / restart 三個 SRE flow。
    """
    if action_type not in ("rollback", "scale", "restart"):
        raise ValueError(f"不支援的 action_type: {action_type!r}")

    if action_type == "rollback":
        command = f"kubectl rollout undo deployment/{target}"
        evidence: dict[str, Any] = {
            "note": f"需確認 deployment/{target} 當前 image 與 rollout history",
            "suggested_verification": f"kubectl rollout history deployment/{target}",
        }
    elif action_type == "scale":
        command = f"kubectl scale deployment/{target} --replicas=<N>"
        evidence = {
            "note": f"需確認 deployment/{target} 當前 replicas 數量",
            "suggested_verification": f"kubectl get deployment/{target} -o json | jq .spec.replicas",
        }
    else:  # restart
        command = f"kubectl rollout restart deployment/{target}"
        evidence = {
            "note": f"需確認 deployment/{target} 當前 pod 狀態",
            "suggested_verification": f"kubectl get pods -l app={target}",
        }

    logger.info(
        "suggest_action_built",
        project_id=project_id,
        run_id=run_id,
        action_type=action_type,
        target=target,
    )

    return SuggestedAction(
        action_type=action_type,
        target=target,
        suggested_command=command,
        rollback_evidence=evidence,
    )
