from __future__ import annotations

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

SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))
for dep_dir in [SCRIPT_DIR / ".pydeps", SCRIPT_DIR.parent / ".pydeps", Path.cwd() / ".pydeps"]:
    if dep_dir.exists() and str(dep_dir) not in sys.path:
        sys.path.insert(0, str(dep_dir))

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

from runtime_config import add_config_source_args, load_runtime_config


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

NOTE_RULES = [
    "ESS独家5折",
    "ESS独家7折",
    "ESS独家",
    "正价RP",
    "礼赠款",
]


@dataclass(frozen=True)
class ReportMonth:
    year: int
    month: int

    @property
    def ym(self) -> str:
        return f"{self.year}-{self.month:02d}"


@dataclass
class TopItem:
    article: str
    qty: int
    note: str
    image_path: Optional[Path]


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_report_month(args: argparse.Namespace, config: dict[str, Any]) -> ReportMonth:
    report_cfg = config.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 ReportMonth(year=year, month=month)


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_fit(
    draw: ImageDraw.ImageDraw,
    box: tuple[int, int, int, int],
    text: str,
    base_size: int,
    *,
    bold: bool = False,
    fill: str = "#1b2738",
    min_size: int = 20,
    padding_x: int = 10,
) -> None:
    x0, y0, x1, y1 = box
    width = max(1, x1 - x0 - padding_x * 2)
    size = base_size
    font = load_font(size, bold=bold)
    while size > min_size:
        bb = draw.textbbox((0, 0), text, font=font)
        if (bb[2] - bb[0]) <= width:
            break
        size -= 1
        font = load_font(size, bold=bold)
    bb = draw.textbbox((0, 0), text, font=font)
    th = bb[3] - bb[1]
    draw.text((x0 + padding_x, y0 + (y1 - y0 - th) / 2), text, font=font, fill=fill)


def fetch_rows(conn: pymysql.connections.Connection, ym: str) -> list[dict[str, Any]]:
    sql = f"""
SELECT category_level_1 AS cat, product_title, item_count_total
FROM `{TABLE_NAME}`
WHERE data_month=%s
"""
    with conn.cursor() as cur:
        cur.execute(sql, (ym,))
        return cur.fetchall() or []


def extract_article(title: str) -> Optional[str]:
    m = re.search(r"(CK\d-[0-9]{8}(?:-[0-9])?)", title.upper())
    if not m:
        return None
    return m.group(1)


def normalize_note(text: str) -> str:
    for k in NOTE_RULES:
        if k in text:
            if k in ("ESS独家5折", "ESS独家7折"):
                return k.replace("折", "\n折")
            return k
    return ""


def build_top_items(rows: list[dict[str, Any]]) -> tuple[list[TopItem], list[TopItem]]:
    # side -> article -> qty
    qty_map: dict[str, dict[str, int]] = {"bags": defaultdict(int), "shoes": defaultdict(int)}
    note_weight: dict[str, dict[str, dict[str, int]]] = {
        "bags": defaultdict(lambda: defaultdict(int)),
        "shoes": defaultdict(lambda: defaultdict(int)),
    }

    for r in rows:
        cat = str(r.get("cat") or "").strip()
        title = str(r.get("product_title") or "")
        article = extract_article(title)
        if not article:
            continue
        qty = int(r.get("item_count_total") or 0)
        if qty <= 0:
            continue
        side = None
        if cat == "箱包":
            side = "bags"
        elif cat == "鞋":
            side = "shoes"
        if side is None:
            continue
        qty_map[side][article] += qty
        note = normalize_note(title)
        if note:
            note_weight[side][article][note] += qty

    def pick(side: str) -> list[TopItem]:
        pairs = sorted(qty_map[side].items(), key=lambda kv: kv[1], reverse=True)[:5]
        out: list[TopItem] = []
        for article, qty in pairs:
            notes = note_weight[side][article]
            note = sorted(notes.items(), key=lambda kv: kv[1], reverse=True)[0][0] if notes else ""
            out.append(TopItem(article=article, qty=qty, note=note, image_path=None))
        return out

    return pick("bags"), pick("shoes")


def fetch_product_image(article: str, cache_dir: Path) -> Optional[Path]:
    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 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 render_panel(
    canvas: Image.Image,
    panel_left: int,
    panel_top: int,
    panel_w: int,
    title: str,
    items: list[TopItem],
    draw: ImageDraw.ImageDraw,
    fonts: dict[str, ImageFont.ImageFont],
) -> None:
    header_h = 50
    row_h = 180
    title_w, pic_w, qty_w = 330, 300, 100
    note_w = panel_w - title_w - pic_w - qty_w
    grid = "#d1d1d1"
    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="#808080", width=2)

    x = panel_left
    cols = [title_w, pic_w, qty_w, note_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 + title_w, panel_top + header_h), title, fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + title_w, panel_top, panel_left + title_w + pic_w, panel_top + header_h), "PIC", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + title_w + pic_w, panel_top, panel_left + title_w + pic_w + qty_w, panel_top + header_h), "QTY", fonts["header"], "#ffffff")
    draw_center(draw, (panel_left + title_w + pic_w + qty_w, panel_top, panel_left + panel_w, panel_top + header_h), "备注", fonts["header"], "#ffffff")

    for i in range(5):
        y0 = panel_top + header_h + i * row_h
        y1 = y0 + row_h
        if i >= len(items):
            continue
        item = items[i]
        draw_left_fit(draw, (panel_left + 8, y0, panel_left + title_w - 8, y1), item.article, 52, fill="#1b2738", min_size=30)
        draw_center(draw, (panel_left + title_w + pic_w, y0, panel_left + title_w + pic_w + qty_w, y1), str(item.qty), fonts["qty"], "#1b2738")
        draw_center(draw, (panel_left + title_w + pic_w + qty_w, y0, panel_left + panel_w, y1), item.note, fonts["note"], "#1b2738")

        pic_box = (panel_left + title_w + 8, y0 + 8, panel_left + title_w + pic_w - 8, y1 - 8)
        if item.image_path and item.image_path.exists():
            img = Image.open(item.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["note"], "#999999")


def render_image(output_path: Path, bags: list[TopItem], shoes: list[TopItem]) -> dict[str, Any]:
    panel_w = 940
    panel_h = 50 + 180 * 5
    gap = 16
    margin = 16
    width = margin * 2 + panel_w * 2 + gap
    height = margin * 2 + panel_h
    canvas = Image.new("RGB", (width, height), "#e7e7e7")
    draw = ImageDraw.Draw(canvas)

    fonts = {
        "header": load_font(34, bold=True),
        "article": load_font(40, bold=False),
        "qty": load_font(42, bold=False),
        "note": load_font(38, bold=False),
    }
    render_panel(canvas, margin, margin, panel_w, "BAGS TOP5", bags, draw, fonts)
    render_panel(canvas, margin + panel_w + gap, margin, panel_w, "SHOES TOP5", shoes, draw, fonts)

    # Downscale for crisp anti-aliasing and smaller size
    out = canvas.resize((width // 2, height // 2), Image.Resampling.LANCZOS)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    out.save(output_path)
    return {
        "bags_top5": [{"article": i.article, "qty": i.qty, "note": i.note} for i in bags],
        "shoes_top5": [{"article": i.article, "qty": i.qty, "note": i.note} for i in shoes],
    }


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate BAGS/SHOES TOP5 image from cep_live_goods_effect.")
    add_config_source_args(parser, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    parser.add_argument("--report-year", type=int, default=0)
    parser.add_argument("--report-month", default="")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    config_path = Path(args.config or r"D:\projects\skills\vip-report\config.yaml").resolve()
    config = load_runtime_config(args, default_config=config_path)
    report_month = resolve_report_month(args, config)

    mysql_cfg = config["mysql"]
    mysql_user = mysql_cfg.get("username") or mysql_cfg.get("user")
    conn = pymysql.connect(
        host=mysql_cfg["host"],
        port=int(mysql_cfg["port"]),
        user=mysql_user,
        password=mysql_cfg["password"],
        database=mysql_cfg["database"],
        charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor,
    )
    try:
        rows = fetch_rows(conn, report_month.ym)
    finally:
        conn.close()

    bags, shoes = build_top_items(rows)

    workdir = Path(config["paths"]["workdir"]).resolve()
    cache_dir = workdir / "data" / "cep-live-goods-effect" / "product-images"
    for item in bags + shoes:
        item.image_path = fetch_product_image(item.article, cache_dir)

    asset_dir = workdir / "assets" / "cep-live-goods-effect"
    data_dir = workdir / "data" / "cep-live-goods-effect"
    image_path = asset_dir / "cep-live-goods-effect-top5.png"
    operations_path = workdir / "render-ops.cep-live-goods-effect-top5.live.json"
    manifest_path = data_dir / "cep-live-goods-effect-top5.live.json"

    metrics = render_image(image_path, bags, shoes)

    operations = {"replace_text": [], "replace_images": []}
    operations_path.write_text(json.dumps(operations, ensure_ascii=False, indent=2), encoding="utf-8")
    manifest = {
        "source": {"table": TABLE_NAME, "month": report_month.ym, "config_path": str(config_path)},
        "asset_path": str(image_path),
        "operations_path": str(operations_path),
        "metrics": metrics,
    }
    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(image_path), "manifest_path": str(manifest_path), "operations_path": str(operations_path)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
