import threading
from datetime import date, datetime, timedelta
from pathlib import Path
from time import monotonic

from PIL import Image, ImageDraw, ImageFont

from app.core.config import settings
from app.core.paths import data_path
from app.models import Certificate, CertificateAccessToken, Learner, ProjectCourse
from app.services.certificate_templates import (
    CLASSIC_TEMPLATE_CODE,
    PRACTICE_CAMP_TEMPLATE_CODE,
    get_certificate_template,
)

DESIGN_WIDTH = 1024
DESIGN_HEIGHT = 759
PDF_BASE_RESOLUTION = 150.0
PDF_GENERATION_WAIT_TIMEOUT_SECONDS = 120


class PdfGenerationBusy(RuntimeError):
    pass


class PdfGenerationLimiter:
    def __init__(self) -> None:
        self._condition = threading.Condition()
        self._active = 0

    def acquire(self, limit: int, timeout_seconds: int = PDF_GENERATION_WAIT_TIMEOUT_SECONDS) -> None:
        deadline = monotonic() + timeout_seconds
        with self._condition:
            while self._active >= limit:
                remaining = deadline - monotonic()
                if remaining <= 0:
                    raise PdfGenerationBusy("当前查询人数较多，请稍后重试")
                self._condition.wait(remaining)
            self._active += 1

    def release(self) -> None:
        with self._condition:
            self._active = max(0, self._active - 1)
            self._condition.notify_all()


pdf_generation_limiter = PdfGenerationLimiter()


def pdf_cache_path(certificate_no: str) -> Path:
    safe_name = certificate_no.replace("/", "_")
    year = safe_name[2:6] if len(safe_name) >= 6 else "misc"
    folder = data_path("pdf-cache") / year
    folder.mkdir(parents=True, exist_ok=True)
    return folder / f"{safe_name}.pdf"


def cached_pdf_is_fresh(path: Path) -> bool:
    if not path.exists():
        return False
    modified_at = datetime.fromtimestamp(path.stat().st_mtime)
    return modified_at >= datetime.now() - timedelta(days=settings.pdf_cache_days)


def update_pdf_access_time(certificate: Certificate) -> None:
    """更新PDF最后访问时间"""
    from datetime import datetime
    certificate.pdf_last_accessed_at = datetime.utcnow()


def mark_pdf_generated(certificate: Certificate, output_path: Path) -> None:
    certificate.pdf_status = "generated"
    certificate.pdf_file_path = str(output_path)
    update_pdf_access_time(certificate)


def certificate_template_path(template_code: str = CLASSIC_TEMPLATE_CODE) -> Path:
    return get_certificate_template(template_code).asset_path


def pdf_resolution(image: Image.Image) -> float:
    return PDF_BASE_RESOLUTION * (image.width / DESIGN_WIDTH)


def render_certificate_pdf(
    certificate: Certificate,
    learner: Learner,
    project: ProjectCourse | None,
    token: CertificateAccessToken,
    concurrency_limit: int = 2,
) -> Path:
    output_path = pdf_cache_path(certificate.certificate_no)
    template_code = getattr(certificate, "template_code", CLASSIC_TEMPLATE_CODE)
    template_path = certificate_template_path(template_code)
    latest_renderer_mtime = max(template_path.stat().st_mtime, Path(__file__).stat().st_mtime)
    if cached_pdf_is_fresh(output_path) and output_path.stat().st_mtime >= latest_renderer_mtime:
        mark_pdf_generated(certificate, output_path)
        return output_path

    pdf_generation_limiter.acquire(max(1, concurrency_limit))
    try:
        if cached_pdf_is_fresh(output_path) and output_path.stat().st_mtime >= latest_renderer_mtime:
            mark_pdf_generated(certificate, output_path)
            return output_path
        image = render_certificate_image(certificate, learner, project)
        image.save(output_path, "PDF", resolution=pdf_resolution(image))
    finally:
        pdf_generation_limiter.release()

    mark_pdf_generated(certificate, output_path)
    return output_path


def render_certificate_image(certificate: Certificate, learner: Learner, project: ProjectCourse | None) -> Image.Image:
    if getattr(certificate, "template_code", CLASSIC_TEMPLATE_CODE) == PRACTICE_CAMP_TEMPLATE_CODE:
        return render_practice_camp_certificate_image(certificate, learner)
    return render_classic_certificate_image(certificate, learner, project)


def render_classic_certificate_image(certificate: Certificate, learner: Learner, project: ProjectCourse | None) -> Image.Image:
    template = certificate_template_path(CLASSIC_TEMPLATE_CODE)
    image = Image.open(template).convert("RGB")
    base_image = image.copy()
    draw = ImageDraw.Draw(image)
    x_scale = image.width / DESIGN_WIDTH
    y_scale = image.height / DESIGN_HEIGHT
    text_scale = (x_scale + y_scale) / 2

    issue_year, issue_month, issue_day = date_parts(certificate.issue_date)
    start_year, start_month, start_day = date_parts(certificate.course_start_date or certificate.issue_date)
    end_year, end_month, end_day = date_parts(certificate.course_end_date or certificate.issue_date)
    course_name = certificate.course_name or certificate.certificate_name or (project.name if project else certificate.project_code)
    stage_input = (certificate.stage_name or "").strip()
    stage_name = f"{stage_input}\u8bfe\u7a0b\u7684\u4e13\u4e1a\u5b66\u4e60\u3002" if stage_input else "\u521d\u7ea7\u8bfe\u7a0b\u7684\u4e13\u4e1a\u5b66\u4e60\u3002"

    paper_patch(image, (150, 455, 760, 119), x_scale, y_scale)

    draw.text(
        (512 * x_scale, 414 * y_scale),
        learner.current_name,
        fill="#111111",
        font=font(32, "kai", True, text_scale),
        anchor="mm",
    )

    x = 185.0 * x_scale
    y = 494 * y_scale
    x = draw_inline(draw, x, y, "\u5728", 24, 18, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, start_year, 24, 8, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u5e74", 24, 12, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, start_month, 24, 6, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u6708", 24, 12, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, start_day, 24, 4, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u65e5\u81f3", 24, 12, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, end_year, 24, 8, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u5e74", 24, 12, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, end_month, 24, 6, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u6708", 24, 12, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, end_day, 24, 4, scale=text_scale, x_scale=x_scale)
    x = draw_inline(draw, x, y, "\u65e5\u5b8c\u6210\u4e86", 24, 8, scale=text_scale, x_scale=x_scale)
    draw_inline_flow(
        draw,
        [(f"\u201c{course_name}\u201d", "course", 0), (stage_name, "normal", 14)],
        x,
        y,
        text_scale,
        x_scale,
        y_scale,
    )

    draw_issue_date_numbers(draw, issue_year, issue_month, issue_day, text_scale, x_scale, y_scale)
    draw.text(
        (105 * x_scale, 76 * y_scale),
        f"\u8bc1\u4e66\u7f16\u53f7\uff1a{certificate.certificate_no}",
        fill="#111827",
        font=font(14, "hei", True, text_scale),
    )
    mentor_box = scaled_box((720, 535, 230, 94), x_scale, y_scale)
    image.paste(base_image.crop(to_xyxy(mentor_box)), mentor_box[:2])
    return image


def render_practice_camp_certificate_image(certificate: Certificate, learner: Learner) -> Image.Image:
    design_width = 1854
    design_height = 1359
    image = Image.open(certificate_template_path(PRACTICE_CAMP_TEMPLATE_CODE)).convert("RGB")
    draw = ImageDraw.Draw(image)
    x_scale = image.width / design_width
    y_scale = image.height / design_height
    text_scale = (x_scale + y_scale) / 2

    start_year, start_month, _ = date_parts(certificate.course_start_date or certificate.issue_date)
    end_year, end_month, _ = date_parts(certificate.course_end_date or certificate.issue_date)
    issue_year, issue_month, issue_day = date_parts(certificate.issue_date)

    draw.text(
        (568 * x_scale, 542 * y_scale),
        learner.current_name,
        fill="#202020",
        font=font(42, "kai", True, text_scale),
        anchor="ms",
    )
    for value, x in [
        (start_year, 474),
        (start_month, 630),
        (end_year, 888),
        (end_month, 1058),
    ]:
        draw.text(
            (x * x_scale, 658 * y_scale),
            value,
            fill="#202020",
            font=font(41, "song", False, text_scale),
            anchor="ms",
        )

    issue_date_text = f"{issue_year}年{issue_month}月{issue_day}日"
    issue_date_font = font(36, "song", False, text_scale)
    issue_date_position = (568 * x_scale, 1149 * y_scale)
    draw.rectangle(
        (
            442 * x_scale,
            1118 * y_scale,
            696 * x_scale,
            1168 * y_scale,
        ),
        fill="#ffffff",
    )
    draw.text(
        issue_date_position,
        issue_date_text,
        fill="#202020",
        font=issue_date_font,
        anchor="ms",
    )
    return image


def font(size: int, kind: str = "song", bold: bool = False, scale: float = 1.0) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    choices = {
        "kai": [
            r"C:\Windows\Fonts\simkai.ttf",
            r"C:\Windows\Fonts\msyh.ttc",
            r"C:\Windows\Fonts\simsun.ttc",
            "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
            "/System/Library/Fonts/Supplemental/Kaiti.ttc",
            ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2),
            ("/System/Library/Fonts/Supplemental/Songti.ttc", 1),
        ],
        "hei": [
            r"C:\Windows\Fonts\simhei.ttf",
            r"C:\Windows\Fonts\msyhbd.ttc",
            r"C:\Windows\Fonts\msyh.ttc",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
            ("/System/Library/Fonts/STHeiti Medium.ttc", 1),
            ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2),
        ],
        "song": [
            r"C:\Windows\Fonts\msyh.ttc",
            r"C:\Windows\Fonts\simsun.ttc",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
            "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
            ("/System/Library/Fonts/Supplemental/Songti.ttc", 3),
            ("/System/Library/Fonts/Supplemental/Songti.ttc", 6),
            ("/System/Library/Fonts/Hiragino Sans GB.ttc", 0),
            ("/System/Library/Fonts/STHeiti Light.ttc", 1),
        ],
    }
    bold_choices = {
        "kai": [
            r"C:\Windows\Fonts\msyhbd.ttc",
            r"C:\Windows\Fonts\simhei.ttf",
            "/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc",
            ("/System/Library/Fonts/Supplemental/Songti.ttc", 0),
            ("/System/Library/Fonts/STHeiti Medium.ttc", 1),
        ],
        "hei": [
            r"C:\Windows\Fonts\msyhbd.ttc",
            r"C:\Windows\Fonts\simhei.ttf",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
            ("/System/Library/Fonts/STHeiti Medium.ttc", 1),
            ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2),
        ],
        "song": [
            r"C:\Windows\Fonts\msyhbd.ttc",
            r"C:\Windows\Fonts\simhei.ttf",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
            ("/System/Library/Fonts/STHeiti Medium.ttc", 1),
            ("/System/Library/Fonts/Hiragino Sans GB.ttc", 2),
        ],
    }
    paths = choices.get(kind, choices["song"])
    if bold:
        paths = bold_choices.get(kind, bold_choices["song"]) + paths
    for item in paths:
        path, index = item if isinstance(item, tuple) else (item, 0)
        if Path(path).exists():
            try:
                return ImageFont.truetype(path, max(1, round(size * scale)), index=index)
            except Exception:
                pass
    return ImageFont.load_default()


def date_parts(value: date | str | None) -> tuple[str, str, str]:
    if isinstance(value, date):
        return str(value.year), str(value.month), str(value.day)
    raw = str(value or "").strip()
    for fmt in ("%Y-%m-%d", "%Y/%m/%d"):
        try:
            d = datetime.strptime(raw[:10], fmt)
            return str(d.year), str(d.month), str(d.day)
        except ValueError:
            pass
    today = date.today()
    return str(today.year), str(today.month), str(today.day)


def scaled_box(box: tuple[int, int, int, int], x_scale: float, y_scale: float) -> tuple[int, int, int, int]:
    x, y, w, h = box
    return round(x * x_scale), round(y * y_scale), round(w * x_scale), round(h * y_scale)


def to_xyxy(box: tuple[int, int, int, int]) -> tuple[int, int, int, int]:
    x, y, w, h = box
    return x, y, x + w, y + h


def paper_patch(image: Image.Image, box: tuple[int, int, int, int], x_scale: float, y_scale: float) -> None:
    x, y, w, h = scaled_box(box, x_scale, y_scale)
    patch = Image.new("RGB", (w, h), "#fbf7ef")
    source = image.crop((round(72 * x_scale), y, round(160 * x_scale), y + h))
    for tx in range(0, w, source.width):
        patch.paste(source.crop((0, 0, min(source.width, w - tx), h)), (tx, 0))
    cover = Image.new("RGB", (w, h), "#fbf7ef")
    image.paste(Image.blend(patch, cover, 0.38), (x, y), soft_edge_mask(w, h, round(16 * x_scale), round(10 * y_scale)))


def soft_edge_mask(width: int, height: int, feather_x: int, feather_y: int) -> Image.Image:
    feather_x = max(1, feather_x)
    feather_y = max(1, feather_y)
    mask = Image.new("L", (width, height), 255)
    pixels = mask.load()
    for y in range(height):
        vertical = min(1.0, y / feather_y, (height - 1 - y) / feather_y)
        for x in range(width):
            horizontal = min(1.0, x / feather_x, (width - 1 - x) / feather_x)
            pixels[x, y] = max(0, min(255, round(255 * min(horizontal, vertical))))
    return mask


def draw_issue_date_numbers(
    draw: ImageDraw.ImageDraw,
    issue_year: str,
    issue_month: str,
    issue_day: str,
    scale: float,
    x_scale: float,
    y_scale: float,
) -> None:
    text_font = font(13, "song", False, scale)
    y = 685 * y_scale
    for value, x in [(issue_year, 500), (issue_month, 535), (issue_day, 566)]:
        draw.text((x * x_scale, y), value, fill="#111111", font=text_font, anchor="mm")


def draw_inline(
    draw: ImageDraw.ImageDraw,
    x: float,
    y: float,
    value: str,
    size: int,
    gap: int,
    kind: str = "song",
    bold: bool = False,
    scale: float = 1.0,
    x_scale: float = 1.0,
) -> float:
    text_font = font(size, kind, bold, scale)
    draw.text((x, y), value, fill="#111111", font=text_font, anchor="ls")
    return x + draw.textlength(value, font=text_font) + gap * x_scale


def draw_course_name(draw: ImageDraw.ImageDraw, value: str, x: float, y: int) -> int:
    max_width = 900 - x
    text_font = font(27, "kai", True)
    if draw.textlength(value, font=text_font) <= max_width:
        draw.text((x, y), value, fill="#111111", font=text_font)
        return y

    start_y = y + 34
    lines = split_by_width(draw, value, text_font, 690)[:2]
    for index, line in enumerate(lines):
        line_y = start_y + index * 34
        draw.text((185, line_y), line, fill="#111111", font=text_font)
    return start_y + (len(lines) - 1) * 34


def draw_inline_flow(
    draw: ImageDraw.ImageDraw,
    segments: list[tuple[str, str, int]],
    start_x: float,
    start_y: float,
    scale: float,
    x_scale: float,
    y_scale: float,
) -> None:
    x = start_x
    y = start_y
    line_start = 185 * x_scale
    line_height = 42 * y_scale
    for value, style, gap_before in segments:
        x += gap_before * x_scale
        text_font = font(27, "kai", True, scale) if style == "course" else font(24, "song", False, scale)
        for char in value:
            max_right = (900 if y == start_y else 730) * x_scale
            width = draw.textlength(char, font=text_font)
            if x + width > max_right and x > line_start:
                x = line_start
                y += line_height
            draw.text((x, y), char, fill="#111111", font=text_font, anchor="ls")
            x += width


def split_by_width(draw: ImageDraw.ImageDraw, value: str, text_font: ImageFont.ImageFont, max_width: int) -> list[str]:
    lines: list[str] = []
    line = ""
    for char in value:
        next_line = line + char
        if line and draw.textlength(next_line, font=text_font) > max_width:
            lines.append(line)
            line = char
        else:
            line = next_line
    if line:
        lines.append(line)
    return lines


def draw_fit(draw: ImageDraw.ImageDraw, pos: tuple[int, int], value: str, text_font: ImageFont.ImageFont, fill: str, max_width: int) -> None:
    font_obj = text_font
    size = getattr(text_font, "size", 24)
    while draw.textlength(value, font=font_obj) > max_width and size > 12:
        size -= 1
        font_obj = font(size, "song", True)
    draw.text(pos, value, fill=fill, font=font_obj)
