from __future__ import annotations

import argparse
import json
import re
import urllib.request
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import pymysql
from PIL import Image, ImageDraw, ImageFont, ImageOps
import yaml


TABLE_NAME = "cep_rejection_goods_analysis"
IMAGE_URL_TMPL = "https://api.charleskeith.cn/img/{article}.jpg"


@dataclass
class TopItem:
    article: str
    reject_qty: int
    reject_rate_qty: float
    major_reason_text: str
    image_path: Path | None


def parse_month_number(raw: Any) -> int:
    if raw is None:
        return 1
    if isinstance(raw, int):
        return min(12, max(1, raw))
    text = str(raw).strip()
    m = re.search(r"(1[0-2]|0?[1-9])", text)
    if m:
        return int(m.group(1))
    return 1


def resolve_month(args: argparse.Namespace, cfg: dict[str, Any]) -> str:
    report_cfg = cfg.get("report", {})
    year = int(args.report_year or report_cfg.get("year", 2026))
    month = parse_month_number(args.report_month or report_cfg.get("month_cn", "1"))
    return f"{year}-{month:02d}"


def load_font(size: int, *, bold: bool = False) -> ImageFont.ImageFont:
    candidates: list[str] = []
    if bold:
        candidates.extend([r"C:\Windows\Fonts\msyhbd.ttc", r"C:\Windows\Fonts\simhei.ttf", r"C:\Windows\Fonts\arialbd.ttf"])
    candidates.extend([r"C:\Windows\Fonts\msyh.ttc", r"C:\Windows\Fonts\simsun.ttc", r"C:\Windows\Fonts\arial.ttf"])
    for p in candidates:
        try:
            return ImageFont.truetype(p, size=size)
        except OSError:
            continue
    return ImageFont.load_default()


def draw_center(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], text: str, font: ImageFont.ImageFont, fill: str) -> None:
    x0, y0, x1, y1 = box
    bb = draw.textbbox((0, 0), text, font=font)
    tw = bb[2] - bb[0]
    th = bb[3] - bb[1]
    draw.text((x0 + (x1 - x0 - tw) / 2, y0 + (y1 - y0 - th) / 2), text, font=font, fill=fill)


def draw_left(draw: ImageDraw.ImageDraw, box: tuple[int, int, int, int], text: str, font: ImageFont.ImageFont, fill: str = "#1b2738") -> None:
    x0, y0, x1, y1 = box
    bb = draw.textbbox((0, 0), text, font=font)
    th = bb[3] - bb[1]
    draw.text((x0 + 10, y0 + (y1 - y0 - th) / 2), text, font=font, fill=fill)


def fit_image_to_box(img: Image.Image, width: int, height: int) -> Image.Image:
    fitted = ImageOps.contain(img, (width, height), Image.Resampling.LANCZOS)
    canvas = Image.new("RGB", (width, height), "#ffffff")
    x = (width - fitted.width) // 2
    y = (height - fitted.height) // 2
    canvas.paste(fitted, (x, y))
    return canvas


def fetch_product_image(article: str, cache_dir: Path) -> Path | None:
    if not article:
        return None
    cache_dir.mkdir(parents=True, exist_ok=True)
    target = cache_dir / f"{article}.jpg"
    if target.exists() and target.stat().st_size > 1000:
        return target
    url = IMAGE_URL_TMPL.format(article=article)
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = resp.read()
        if len(data) < 1000:
            return None
        target.write_bytes(data)
        return target
    except Exception:
        return None


def fetch_rows(conn: pymysql.connections.Connection, data_month: str) -> list[dict[str, Any]]:
    sql = f"""
SELECT article, reason_level_1, reject_scene_level_2, reject_qty, reject_rate_qty
FROM `{TABLE_NAME}`
WHERE data_month=%s
"""
    with conn.cursor() as cur:
        cur.execute(sql, (data_month,))
        return cur.fetchall() or []


def build_top_items(rows: list[dict[str, Any]]) -> tuple[list[TopItem], list[TopItem]]:
    # total rows: 一级拒退原因=全部原因
    totals: dict[str, dict[str, float]] = {}
    reason_map: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))

    for r in rows:
        article = str(r.get("article") or "").strip()
        if not article:
            continue
        reason1 = str(r.get("reason_level_1") or "").strip()
        scene2 = str(r.get("reject_scene_level_2") or "").strip()
        qty = int(r.get("reject_qty") or 0)
        rate = float(r.get("reject_rate_qty") or 0.0)

        if reason1 == "全部原因":
            totals[article] = {"qty": qty, "rate": rate}
            continue
        if scene2 and scene2 != "全部":
            reason_map[article][scene2] += max(0, qty)

    def make_side(prefix: str) -> list[TopItem]:
        candidates = [(a, v["qty"], v["rate"]) for a, v in totals.items() if a.startswith(prefix)]
        candidates.sort(key=lambda x: x[1], reverse=True)
        out: list[TopItem] = []
        for article, qty, rate in candidates[:5]:
            rs = sorted(reason_map.get(article, {}).items(), key=lambda kv: kv[1], reverse=True)[:2]
            lines: list[str] = []
            for name, c in rs:
                pct = int(round((c / qty) * 100)) if qty > 0 else 0
                lines.append(f"{name}{pct}%")
            reason_text = "\n".join(lines) if lines else ""
            out.append(
                TopItem(
                    article=article,
                    reject_qty=int(qty),
                    reject_rate_qty=float(rate),
                    major_reason_text=reason_text,
                    image_path=None,
                )
            )
        return out

    return make_side("CK2"), make_side("CK1")


def render_panel(
    canvas: Image.Image,
    panel_left: int,
    panel_top: int,
    panel_w: int,
    items: list[TopItem],
    draw: ImageDraw.ImageDraw,
    fonts: dict[str, ImageFont.ImageFont],
) -> None:
    header_h = 42
    row_h = 138
    article_w, pic_w, qty_w, rate_w = 215, 218, 120, 132
    reason_w = panel_w - article_w - pic_w - qty_w - rate_w

    grid = "#8f8f8f"
    header_bg = "#7279de"

    draw.rectangle((panel_left, panel_top, panel_left + panel_w, panel_top + header_h), fill=header_bg, outline=grid, width=1)
    draw.rectangle((panel_left, panel_top, panel_left + panel_w, panel_top + header_h + row_h * 5), outline=grid, width=2)

    x = panel_left
    cols = [article_w, pic_w, qty_w, rate_w, reason_w]
    for w in cols[:-1]:
        x += w
        draw.line((x, panel_top, x, panel_top + header_h + row_h * 5), fill=grid, width=1)
    for i in range(1, 6):
        y = panel_top + header_h + i * row_h
        draw.line((panel_left, y, panel_left + panel_w, y), fill=grid, width=1)

    draw_center(draw, (panel_left, panel_top, panel_left + article_w, panel_top + header_h), "拒退QTY TOP", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + article_w, panel_top, panel_left + article_w + pic_w, panel_top + header_h), "PIC", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + article_w + pic_w, panel_top, panel_left + article_w + pic_w + qty_w, panel_top + header_h), "拒退件数", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + article_w + pic_w + qty_w, panel_top, panel_left + article_w + pic_w + qty_w + rate_w, panel_top + header_h), "拒退率", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + article_w + pic_w + qty_w + rate_w, panel_top, panel_left + panel_w, panel_top + header_h), "主要拒退原因", fonts["header"], "#ffffff")

    for i in range(5):
        if i >= len(items):
            continue
        it = items[i]
        y0 = panel_top + header_h + i * row_h
        y1 = y0 + row_h
        draw_left(draw, (panel_left + 8, y0, panel_left + article_w - 8, y1), it.article, fonts["article"])
        draw_center(draw, (panel_left + article_w + pic_w, y0, panel_left + article_w + pic_w + qty_w, y1), str(it.reject_qty), fonts["value"], "#1b2738")
        draw_center(draw, (panel_left + article_w + pic_w + qty_w, y0, panel_left + article_w + pic_w + qty_w + rate_w, y1), f"{it.reject_rate_qty:.2f}%", fonts["value"], "#1b2738")
        draw_center(draw, (panel_left + article_w + pic_w + qty_w + rate_w, y0, panel_left + panel_w, y1), it.major_reason_text, fonts["reason"], "#1b2738")

        pic_box = (panel_left + article_w + 8, y0 + 8, panel_left + article_w + pic_w - 8, y1 - 8)
        if it.image_path and it.image_path.exists():
            img = Image.open(it.image_path).convert("RGB")
            fitted = fit_image_to_box(img, pic_box[2] - pic_box[0], pic_box[3] - pic_box[1])
            canvas.paste(fitted, (pic_box[0], pic_box[1]))
        else:
            draw.rectangle(pic_box, fill="#f2f2f2", outline="#d0d0d0", width=1)
            draw_center(draw, pic_box, "NO PIC", fonts["reason"], "#999999")


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Render rejection goods top chart from cep_rejection_goods_analysis.")
    p.add_argument("--config", default=r"D:\projects\skills\vip-report\config.yaml")
    p.add_argument("--report-year", type=int, default=0)
    p.add_argument("--report-month", default="")
    p.add_argument("--output-scale", type=float, default=2.0)
    return p.parse_args()


def main() -> None:
    args = parse_args()
    config_path = Path(args.config).resolve()
    cfg = yaml.safe_load(config_path.read_text(encoding="utf-8"))
    data_month = resolve_month(args, cfg)

    mysql_cfg = cfg["mysql"]
    conn = pymysql.connect(
        host=mysql_cfg["host"],
        port=int(mysql_cfg.get("port", 3306)),
        user=mysql_cfg.get("username") or mysql_cfg.get("user"),
        password=mysql_cfg["password"],
        database=mysql_cfg["database"],
        charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor,
    )
    try:
        rows = fetch_rows(conn, data_month)
    finally:
        conn.close()
    if not rows:
        raise RuntimeError(f"No rows in {TABLE_NAME} for month={data_month}")

    bags, shoes = build_top_items(rows)

    workdir = Path(cfg["paths"]["workdir"]).resolve()
    cache_dir = workdir / "data" / "cep-rejection-analysis" / "product-images"
    for it in bags + shoes:
        it.image_path = fetch_product_image(it.article, cache_dir)

    panel_w = 916
    panel_h = 42 + 138 * 5
    gap = 16
    margin = 10
    canvas_w = margin * 2 + panel_w * 2 + gap
    canvas_h = margin * 2 + panel_h
    canvas = Image.new("RGB", (canvas_w, canvas_h), "#e7e7e7")
    draw = ImageDraw.Draw(canvas)
    fonts = {
        "header": load_font(25, bold=True),
        "article": load_font(38, bold=False),
        "value": load_font(38, bold=False),
        "reason": load_font(31, bold=False),
    }

    render_panel(canvas, margin, margin, panel_w, bags, draw, fonts)
    render_panel(canvas, margin + panel_w + gap, margin, panel_w, shoes, draw, fonts)

    scale = max(0.25, float(args.output_scale))
    out_w = max(1, int(round((canvas_w // 2) * scale)))
    out_h = max(1, int(round((canvas_h // 2) * scale)))
    out = canvas.resize((out_w, out_h), Image.Resampling.LANCZOS)
    output_path = workdir / "assets" / "cep-rejection-analysis" / "cep-rejection-goods-top5.png"
    output_path.parent.mkdir(parents=True, exist_ok=True)
    out.save(output_path)

    manifest = {
        "source_table": TABLE_NAME,
        "month": data_month,
        "bags_top5": [{"article": i.article, "reject_qty": i.reject_qty, "reject_rate_qty": i.reject_rate_qty, "major_reason_text": i.major_reason_text} for i in bags],
        "shoes_top5": [{"article": i.article, "reject_qty": i.reject_qty, "reject_rate_qty": i.reject_rate_qty, "major_reason_text": i.major_reason_text} for i in shoes],
        "image_path": str(output_path),
    }
    manifest_path = workdir / "data" / "cep-rejection-analysis" / "cep-rejection-goods-top5.live.json"
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps({"image_path": str(output_path), "manifest_path": str(manifest_path)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
