from __future__ import annotations

import base64
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from io import BytesIO
import logging
import random
import string
from uuid import uuid4

from fastapi import HTTPException, status
from PIL import Image, ImageDraw, ImageFilter, ImageFont

from app.services.redis_client import get_sync_redis_client

logger = logging.getLogger(__name__)

CAPTCHA_EXPIRES_SECONDS = 300
CAPTCHA_WIDTH = 168
CAPTCHA_HEIGHT = 64
CAPTCHA_LENGTH = 4


@dataclass
class CaptchaRecord:
    code: str
    expires_at: datetime


class CaptchaService:
    _records: dict[str, CaptchaRecord] = {}

    @classmethod
    def create(cls) -> dict:
        cls._cleanup_expired()
        captcha_id = str(uuid4())
        code = _random_code()
        if _set_redis_captcha(captcha_id, code):
            cls._records.pop(captcha_id, None)
        else:
            cls._records[captcha_id] = CaptchaRecord(
                code=code,
                expires_at=datetime.now(UTC) + timedelta(seconds=CAPTCHA_EXPIRES_SECONDS),
            )
        return {
            "captchaId": captcha_id,
            "imageBase64": _render_captcha_image(code),
            "expiresInSeconds": CAPTCHA_EXPIRES_SECONDS,
        }

    @classmethod
    def verify(cls, captcha_id: str, captcha_code: str) -> None:
        redis_available, redis_code = _get_redis_captcha(captcha_id)
        if redis_available and redis_code is not None:
            if redis_code.lower() != captcha_code.strip().lower():
                raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="图形验证码错误")
            _delete_redis_captcha(captcha_id)
            cls._records.pop(captcha_id, None)
            return

        record = cls._records.get(captcha_id)
        if record is None:
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="图形验证码不存在或已过期")
        if record.expires_at < datetime.now(UTC):
            cls._records.pop(captcha_id, None)
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="图形验证码已过期")
        if record.code.lower() != captcha_code.strip().lower():
            raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="图形验证码错误")
        cls._records.pop(captcha_id, None)

    @classmethod
    def _cleanup_expired(cls) -> None:
        now = datetime.now(UTC)
        expired_ids = [captcha_id for captcha_id, record in cls._records.items() if record.expires_at < now]
        for captcha_id in expired_ids:
            cls._records.pop(captcha_id, None)


def _random_code() -> str:
    alphabet = "".join(ch for ch in string.ascii_uppercase + string.digits if ch not in "0O1IL")
    return "".join(random.SystemRandom().choice(alphabet) for _ in range(CAPTCHA_LENGTH))


def _captcha_key(captcha_id: str) -> str:
    return f"auth:captcha:{captcha_id}"


def _set_redis_captcha(captcha_id: str, code: str) -> bool:
    client = get_sync_redis_client()
    if client is None:
        return False
    try:
        return bool(client.set(_captcha_key(captcha_id), code, ex=CAPTCHA_EXPIRES_SECONDS))
    except Exception:
        logger.warning("Redis captcha write failed for %s", captcha_id, exc_info=True)
        return False


def _get_redis_captcha(captcha_id: str) -> tuple[bool, str | None]:
    client = get_sync_redis_client()
    if client is None:
        return False, None
    try:
        value = client.get(_captcha_key(captcha_id))
    except Exception:
        logger.warning("Redis captcha read failed for %s", captcha_id, exc_info=True)
        return False, None
    return True, value if isinstance(value, str) else None


def _delete_redis_captcha(captcha_id: str) -> None:
    client = get_sync_redis_client()
    if client is None:
        return
    try:
        client.delete(_captcha_key(captcha_id))
    except Exception:
        logger.warning("Redis captcha delete failed for %s", captcha_id, exc_info=True)


def _render_captcha_image(code: str) -> str:
    image = Image.new("RGB", (CAPTCHA_WIDTH, CAPTCHA_HEIGHT), "#f7fbfa")
    draw = ImageDraw.Draw(image)
    rng = random.SystemRandom()

    for _ in range(70):
        x = rng.randint(0, CAPTCHA_WIDTH - 1)
        y = rng.randint(0, CAPTCHA_HEIGHT - 1)
        color = rng.choice(["#d7e5df", "#c7ddd5", "#e4ece9"])
        draw.point((x, y), fill=color)

    for _ in range(4):
        start = (rng.randint(0, CAPTCHA_WIDTH // 2), rng.randint(0, CAPTCHA_HEIGHT))
        end = (rng.randint(CAPTCHA_WIDTH // 2, CAPTCHA_WIDTH), rng.randint(0, CAPTCHA_HEIGHT))
        draw.line([start, end], fill=rng.choice(["#b7cec5", "#cbdad5"]), width=1)

    font = _load_font()
    char_width = CAPTCHA_WIDTH // CAPTCHA_LENGTH
    for index, char in enumerate(code):
        layer = Image.new("RGBA", (char_width, CAPTCHA_HEIGHT), (255, 255, 255, 0))
        layer_draw = ImageDraw.Draw(layer)
        bbox = layer_draw.textbbox((0, 0), char, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
        x = (char_width - text_width) // 2
        y = (CAPTCHA_HEIGHT - text_height) // 2 - 3
        layer_draw.text((x, y), char, font=font, fill=rng.choice(["#0f735d", "#173f35", "#31564d"]))
        layer = layer.rotate(rng.randint(-14, 14), resample=Image.Resampling.BICUBIC, expand=False)
        image.paste(layer, (index * char_width, 0), layer)

    image = image.filter(ImageFilter.SMOOTH)
    output = BytesIO()
    image.save(output, format="PNG")
    encoded = base64.b64encode(output.getvalue()).decode("ascii")
    return f"data:image/png;base64,{encoded}"


def _load_font():
    for path in (
        "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
        "/usr/local/share/fonts/DejaVuSans-Bold.ttf",
        "/System/Library/Fonts/Supplemental/Arial Bold.ttf",
        "/Library/Fonts/Arial Bold.ttf",
    ):
        try:
            return ImageFont.truetype(path, 38)
        except OSError:
            continue
    logger.warning("Captcha font fallback to Pillow default font")
    return ImageFont.load_default()
