from __future__ import annotations

import argparse
import json
import sys
from decimal import Decimal
from pathlib import Path
from typing import Any

import pymysql
from PIL import Image, ImageDraw, ImageFont
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from runtime_config import add_config_source_args, load_runtime_config


MONTH_CN_MAP = {
    1: "\u4e00\u6708",
    2: "\u4e8c\u6708",
    3: "\u4e09\u6708",
    4: "\u56db\u6708",
    5: "\u4e94\u6708",
    6: "\u516d\u6708",
    7: "\u4e03\u6708",
    8: "\u516b\u6708",
    9: "\u4e5d\u6708",
    10: "\u5341\u6708",
    11: "\u5341\u4e00\u6708",
    12: "\u5341\u4e8c\u6708",
}
MONTH_CN_TO_NUM = {label: number for number, label in MONTH_CN_MAP.items()}

HEX_TRAFFIC_SUMMARY = "E6B187E680BB"  # 汇总
ENTRY_ORDER = [
    "首页限时狂秒",
    "签到有礼",
    "消息中心",
    "奥莱",
    "天天低价",
    "其他",
    "购物车",
    "搜索",
    "首页",
    "我的特卖",
    "底bar栏目",
    "顶部导航",
    "频道广场",
    "个人中心",
    "唯触点",
    "唯智展",
    "唯直达",
    "Target-Max",
    "新人三单礼",
]

def normalize_month_label(raw_month: Any) -> str:
    if raw_month is None:
        return MONTH_CN_MAP[1]
    if isinstance(raw_month, int):
        return MONTH_CN_MAP.get(raw_month, MONTH_CN_MAP[1])
    text = str(raw_month).strip()
    if text in MONTH_CN_TO_NUM:
        return text
    if text.isdigit():
        value = int(text)
        if 1 <= value <= 12:
            return MONTH_CN_MAP[value]
    return text


def month_label_to_number(label: str) -> int:
    return MONTH_CN_TO_NUM.get(label, 1)


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"])
    candidates.extend([r"C:\Windows\Fonts\msyh.ttc", r"C:\Windows\Fonts\simsun.ttc", r"C:\Windows\Fonts\arial.ttf"])
    for path in candidates:
        try:
            return ImageFont.truetype(path, size=size)
        except OSError:
            continue
    return ImageFont.load_default()


def _measure(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> tuple[int, int]:
    box = draw.textbbox((0, 0), text, font=font)
    return box[2] - box[0], box[3] - box[1]


def _draw_rotated_text(
    base: Image.Image,
    *,
    x: int,
    y: int,
    text: str,
    font: ImageFont.ImageFont,
    fill: str,
    angle: float,
) -> None:
    # Draw tightly around text then rotate, so anchor won't drift off-canvas.
    probe = Image.new("RGBA", (8, 8), (0, 0, 0, 0))
    pd = ImageDraw.Draw(probe)
    bbox = pd.textbbox((0, 0), text, font=font)
    tw = max(1, bbox[2] - bbox[0])
    th = max(1, bbox[3] - bbox[1])
    tmp = Image.new("RGBA", (tw + 8, th + 8), (0, 0, 0, 0))
    d = ImageDraw.Draw(tmp)
    d.text((4, 4), text, font=font, fill=fill)
    rot = tmp.rotate(angle, expand=True)
    base.alpha_composite(rot, (x - rot.width // 2, y))


def fetch_entry_uv_map(
    conn: pymysql.connections.Connection,
    *,
    daily_month: str,
) -> dict[str, Decimal]:
    sql = f"""
SELECT
  primary_entry,
  SUM(IFNULL(product_detail_uv, 0)) AS uv
FROM cep_traffic_summary
WHERE daily=%s
  AND COALESCE(secondary_entry, '')='all'
  AND COALESCE(primary_entry, '')<>'' 
  AND COALESCE(primary_entry, '')<>'all'
  AND HEX(traffic_type) <> '{HEX_TRAFFIC_SUMMARY}'
GROUP BY primary_entry
"""
    with conn.cursor() as cursor:
        cursor.execute(sql, (daily_month,))
        rows = cursor.fetchall() or []
    result: dict[str, Decimal] = {}
    for row in rows:
        entry = str(row.get("primary_entry") or "").strip()
        if not entry:
            continue
        result[entry] = Decimal(str(row.get("uv") or 0))
    return result


def _draw_donut(
    draw: ImageDraw.ImageDraw,
    *,
    box: tuple[int, int, int, int],
    title: str,
    entry_map: dict[str, Decimal],
) -> None:
    title_font = load_font(20, bold=True)
    label_font = load_font(13, bold=False)
    center_font = load_font(14, bold=True)

    x0, y0, x1, y1 = box
    tw, _ = _measure(draw, title, title_font)
    draw.text((x0 + (x1 - x0 - tw) // 2, y0 + 2), title, fill="#444444", font=title_font)

    cx = (x0 + x1) // 2
    cy = (y0 + y1) // 2 + 18
    r_outer = min((x1 - x0), (y1 - y0)) // 2 - 26
    r_inner = r_outer - 34

    total = sum(entry_map.values())
    if total <= 0:
        return

    palette = [
        "#4f81bd", "#c0504d", "#9bbb59", "#8064a2", "#4bacc6", "#f79646",
        "#92a9cf", "#d99694", "#c3d69b", "#b2a1c7", "#7f7f7f", "#9f4c24",
    ]

    items = sorted(entry_map.items(), key=lambda kv: kv[1], reverse=True)
    top_items = items[:10]
    other_value = sum(v for _, v in items[10:])
    if other_value > 0:
        top_items.append(("Other", other_value))

    start = -90.0
    for idx, (name, value) in enumerate(top_items):
        ratio = float(value / total)
        sweep = 360.0 * ratio
        color = palette[idx % len(palette)]
        draw.pieslice((cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer), start, start + sweep, fill=color, outline="#ffffff", width=1)
        mid = (start + start + sweep) / 2.0

        # label point
        import math
        lx = cx + int((r_outer + 12) * math.cos(math.radians(mid)))
        ly = cy + int((r_outer + 12) * math.sin(math.radians(mid)))
        text = f"{name}, {int(round(ratio * 100))}%"
        tw, th = _measure(draw, text, label_font)
        if lx >= cx:
            tx = lx + 4
        else:
            tx = lx - tw - 4
        ty = ly - th // 2
        tx = max(x0 + 2, min(tx, x1 - tw - 2))
        ty = max(y0 + 28, min(ty, y1 - th - 2))
        draw.line([(cx + int((r_outer - 2) * math.cos(math.radians(mid))), cy + int((r_outer - 2) * math.sin(math.radians(mid)))), (lx, ly)], fill="#777777", width=1)
        draw.text((tx, ty), text, fill="#444444", font=label_font)
        start += sweep

    draw.ellipse((cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner), fill="#efefef")
    main_name, main_value = top_items[0]
    main_pct = int(round(float(main_value / total) * 100))
    center_text = f"{main_name}, {main_pct}%"
    tw, th = _measure(draw, center_text, center_font)
    draw.text((cx - tw // 2, cy - th // 2), center_text, fill="#555555", font=center_font)


def render_chart(
    *,
    output_path: Path,
    report_year: int,
    compare_year: int,
    cur_map: dict[str, Decimal],
    prev_map: dict[str, Decimal],
) -> None:
    img = Image.new("RGBA", (2120, 420), "#e7e7e7")
    draw = ImageDraw.Draw(img)
    title_font = load_font(28, bold=True)
    legend_font = load_font(15, bold=False)
    axis_font = load_font(14, bold=False)

    # Left: bar chart
    left_box = (20, 8, 1040, 400)
    lx0, ly0, lx1, ly1 = left_box
    draw.text((lx0 + 300, ly0 + 2), "Entry Traffic LFL", fill="#444444", font=title_font)
    draw.rectangle((lx0, ly0, lx1, ly1), outline=None, fill="#e7e7e7")

    bar_left = lx0 + 48
    bar_top = ly0 + 78
    bar_right = lx1 - 16
    bar_bottom = ly1 - 110
    draw.line([(bar_left, bar_bottom), (bar_right, bar_bottom)], fill="#999999", width=1)
    draw.line([(bar_left, bar_top), (bar_left, bar_bottom)], fill="#999999", width=1)

    # legend
    draw.rectangle((lx0 + 330, ly0 + 42, lx0 + 342, ly0 + 50), fill="#6f76ff")
    draw.text((lx0 + 346, ly0 + 36), f"{str(report_year)[-2:]}UV", fill="#444444", font=legend_font)
    draw.rectangle((lx0 + 410, ly0 + 42, lx0 + 422, ly0 + 50), fill="#f08ad9")
    draw.text((lx0 + 426, ly0 + 36), f"{str(compare_year)[-2:]}UV", fill="#444444", font=legend_font)

    union_entries = set(cur_map) | set(prev_map)
    entries = [e for e in ENTRY_ORDER if e in union_entries]
    extras = sorted(
        [e for e in union_entries if e not in entries],
        key=lambda k: (cur_map.get(k, Decimal("0")) + prev_map.get(k, Decimal("0"))),
        reverse=True,
    )
    entries.extend(extras)
    max_v = int(max([0] + [int(cur_map.get(e, 0)) for e in entries] + [int(prev_map.get(e, 0)) for e in entries]))
    if max_v <= 0:
        max_v = 1
    y_step = 100000
    y_max = ((max_v + y_step - 1) // y_step) * y_step

    # Keep baseline at 0, but show labeled ticks starting from 100000.
    for yv in range(y_step, y_max + 1, y_step):
        y = bar_bottom - int(yv / y_max * (bar_bottom - bar_top))
        draw.line([(bar_left, y), (bar_right, y)], fill="#cdcdcd", width=1)
        label = str(yv)
        tw, th = _measure(draw, label, axis_font)
        draw.text((bar_left - tw - 6, y - th // 2), label, fill="#555555", font=axis_font)

    if entries:
        slot = (bar_right - bar_left) / len(entries)
        bw = max(4, int(slot * 0.28))
        for i, e in enumerate(entries):
            cx = bar_left + int((i + 0.5) * slot)
            cur = int(cur_map.get(e, Decimal("0")))
            prv = int(prev_map.get(e, Decimal("0")))
            ch = int(cur / y_max * (bar_bottom - bar_top))
            ph = int(prv / y_max * (bar_bottom - bar_top))
            draw.rectangle((cx - bw - 1, bar_bottom - ch, cx - 1, bar_bottom), fill="#6f76ff")
            draw.rectangle((cx + 1, bar_bottom - ph, cx + bw + 1, bar_bottom), fill="#f08ad9")
            # x label
            label = e
            ty = bar_bottom + 8
            _draw_rotated_text(img, x=cx, y=ty, text=label, font=axis_font, fill="#555555", angle=35)

    # Right: two donuts
    _draw_donut(draw, box=(1060, 8, 1580, 400), title=f"{str(report_year)[-2:]} Entry Mix", entry_map=cur_map)
    _draw_donut(draw, box=(1600, 8, 2110, 400), title=f"{str(compare_year)[-2:]} Entry Mix", entry_map=prev_map)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    img.convert("RGB").save(output_path)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate entry traffic chart from cep_traffic_summary.")
    add_config_source_args(parser, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    parser.add_argument("--report-month", default="", help="Month override, e.g. Jan / 1")
    parser.add_argument("--report-year", type=int, default=0, help="Report year override")
    parser.add_argument("--compare-year", type=int, default=0, help="Compare year override")
    return parser.parse_args()


def execute(
    config: dict[str, Any],
    *,
    config_ref: str = "",
    report_month: str = "",
    report_year: int = 0,
    compare_year: int = 0,
) -> dict[str, str]:
    config_path = config_ref or "<runtime-config>"
    report_cfg = config.get("report", {})
    month_label = normalize_month_label(report_month or report_cfg.get("month_cn", MONTH_CN_MAP[1]))
    month_num = month_label_to_number(month_label)
    report_year = int(report_year or report_cfg.get("year", 2026))
    compare_year = int(compare_year or report_cfg.get("compare_year", report_year - 1))

    report_daily = f"{report_year}-{month_num:02d}"
    compare_daily = f"{compare_year}-{month_num:02d}"

    mysql_cfg = config["mysql"]
    conn = pymysql.connect(
        host=mysql_cfg["host"],
        port=int(mysql_cfg["port"]),
        user=mysql_cfg["username"],
        password=mysql_cfg["password"],
        database=mysql_cfg["database"],
        charset="utf8mb4",
        cursorclass=pymysql.cursors.DictCursor,
    )
    try:
        cur_map = fetch_entry_uv_map(conn, daily_month=report_daily)
        prev_map = fetch_entry_uv_map(conn, daily_month=compare_daily)
    finally:
        conn.close()

    vip_workdir = Path(config["paths"]["workdir"]).resolve()
    asset_dir = vip_workdir / "assets" / "cep-traffic-summary"
    data_dir = vip_workdir / "data" / "cep-traffic-summary"
    image_path = asset_dir / "cep-traffic-summary-lfl.png"
    operations_path = vip_workdir / "render-ops.cep-traffic-summary.live.json"
    manifest_path = data_dir / "cep-traffic-summary-lfl.live.json"

    render_chart(
        output_path=image_path,
        report_year=report_year,
        compare_year=compare_year,
        cur_map=cur_map,
        prev_map=prev_map,
    )

    operations = {"replace_text": [], "replace_images": []}
    operations_path.write_text(json.dumps(operations, ensure_ascii=False, indent=2), encoding="utf-8")

    manifest = {
        "source": {
            "table": "cep_traffic_summary",
            "daily_report": report_daily,
            "daily_compare": compare_daily,
            "config_path": str(config_path),
        },
        "summary": {
            "entry_count_report": len(cur_map),
            "entry_count_compare": len(prev_map),
            "report_top": sorted([(k, int(v)) for k, v in cur_map.items()], key=lambda x: x[1], reverse=True)[:20],
            "compare_top": sorted([(k, int(v)) for k, v in prev_map.items()], key=lambda x: x[1], reverse=True)[:20],
        },
        "asset_path": str(image_path),
        "operations_path": str(operations_path),
    }
    manifest_path.parent.mkdir(parents=True, exist_ok=True)
    manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")

    return {
        "operations_path": str(operations_path),
        "manifest_path": str(manifest_path),
        "image_path": str(image_path),
    }


def main() -> None:
    args = parse_args()
    config = load_runtime_config(args, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    result = execute(
        config,
        config_ref=(args.config if args.config_source == "yaml" else f"db:{args.config_table}/{args.config_key}"),
        report_month=args.report_month,
        report_year=args.report_year,
        compare_year=args.compare_year,
    )
    print(json.dumps(result, ensure_ascii=False))


if __name__ == "__main__":
    main()
