from __future__ import annotations

import argparse
import calendar
import json
import sys
from decimal import Decimal, ROUND_HALF_UP
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()}


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 to_decimal(value: Any) -> Decimal:
    if value is None:
        return Decimal("0")
    if isinstance(value, Decimal):
        return value
    text = str(value).strip()
    if not text:
        return Decimal("0")
    text = text.replace(",", "")
    try:
        return Decimal(text)
    except Exception:
        return Decimal("0")


def round_up(value: int, step: int) -> int:
    if value <= 0:
        return step
    return ((value + step - 1) // step) * step


def fetch_month_uv_series(
    conn: pymysql.connections.Connection,
    *,
    year: int,
    month: int,
) -> dict[str, list[Decimal]]:
    prefix = f"{year}{month:02d}"
    sql = """
SELECT
  biz_date,
  traffic_type,
  SUM(IFNULL(product_detail_uv, 0)) AS uv
FROM cep_site_traffic_entry
WHERE biz_date LIKE %s
  AND COALESCE(primary_entry, '') = 'all'
  AND COALESCE(secondary_entry, '') = 'all'
GROUP BY biz_date, traffic_type
"""
    with conn.cursor() as cursor:
        cursor.execute(sql, (f"{prefix}%",))
        rows = cursor.fetchall() or []

    days_in_month = calendar.monthrange(year, month)[1]
    free = [Decimal("0")] * days_in_month
    paid = [Decimal("0")] * days_in_month
    for row in rows:
        biz_date = str(row.get("biz_date") or "").strip()
        if len(biz_date) != 8:
            continue
        day = int(biz_date[-2:])
        if day < 1 or day > days_in_month:
            continue
        uv = to_decimal(row.get("uv"))
        ttype = str(row.get("traffic_type") or "").strip()
        if ("自然" in ttype) or ("free" in ttype.lower()):
            free[day - 1] = uv
        elif ("付费" in ttype) or ("paid" in ttype.lower()):
            paid[day - 1] = uv

    return {"free": free, "paid": paid}


def lfl_series(current: list[Decimal], previous: list[Decimal]) -> list[Decimal]:
    values: list[Decimal] = []
    for c, p in zip(current, previous):
        if p == 0:
            values.append(Decimal("0"))
        else:
            values.append((c - p) / p * Decimal("100"))
    return values


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_legend(
    draw: ImageDraw.ImageDraw,
    *,
    x: int,
    y: int,
    labels: list[tuple[str, str, str]],
    font: ImageFont.ImageFont,
) -> None:
    # label tuple: (text, color, kind[line|dot])
    offset_x = x
    for text, color, kind in labels:
        if kind == "line":
            draw.line([(offset_x, y + 8), (offset_x + 26, y + 8)], fill=color, width=3)
        else:
            draw.ellipse((offset_x + 8, y + 4, offset_x + 14, y + 10), fill=color, outline=color)
        offset_x += 32
        draw.text((offset_x, y), text, fill="#333333", font=font)
        tw, _ = _measure(draw, text, font)
        offset_x += tw + 18


def _nice_lfl_bounds(values: list[Decimal]) -> tuple[int, int, int]:
    if not values:
        return (-20, 60, 20)
    min_v = float(min(values))
    max_v = float(max(values))
    span = max(abs(min_v), abs(max_v))
    if span <= 80:
        step = 20
    elif span <= 300:
        step = 50
    else:
        step = 100
    low = int((min_v // step) * step)
    high = int(((max_v + step - 1) // step) * step)
    if low > 0:
        low = 0
    if high < 0:
        high = 0
    if high == low:
        high += step
    return low, high, step


def _draw_panel(
    draw: ImageDraw.ImageDraw,
    *,
    panel_box: tuple[int, int, int, int],
    title: str,
    current_label: str,
    previous_label: str,
    current_values: list[Decimal],
    previous_values: list[Decimal],
    lfl_values: list[Decimal],
    month: int,
) -> None:
    x0, y0, x1, y1 = panel_box
    title_font = load_font(24, bold=True)
    legend_font = load_font(16, bold=False)
    axis_font = load_font(14, bold=False)

    chart_left = x0 + 64
    chart_top = y0 + 80
    chart_right = x1 - 56
    chart_bottom = y1 - 58
    chart_w = chart_right - chart_left
    chart_h = chart_bottom - chart_top

    title_w, _ = _measure(draw, title, title_font)
    draw.text((x0 + (x1 - x0 - title_w) / 2, y0 + 2), title, fill="#333333", font=title_font)
    _draw_legend(
        draw,
        x=x0 + 220,
        y=y0 + 44,
        labels=[(current_label, "#6f76ff", "line"), (previous_label, "#f08ad9", "line"), ("LFL", "#b000c9", "dot")],
        font=legend_font,
    )

    max_volume = max(int(max(current_values or [Decimal("0")])), int(max(previous_values or [Decimal("0")])))
    y_step = 5000 if max_volume > 15000 else 2000
    y_max = round_up(max_volume, y_step)

    lfl_low, lfl_high, lfl_step = _nice_lfl_bounds(lfl_values)

    # grid + left/right axis labels
    for v in range(0, y_max + 1, y_step):
        y = chart_bottom - int(v / y_max * chart_h) if y_max else chart_bottom
        draw.line([(chart_left, y), (chart_right, y)], fill="#cfcfcf", width=1)
        label = str(v)
        tw, th = _measure(draw, label, axis_font)
        draw.text((chart_left - tw - 8, y - th / 2), label, fill="#555555", font=axis_font)

    for v in range(lfl_low, lfl_high + 1, lfl_step):
        ratio = (v - lfl_low) / (lfl_high - lfl_low)
        y = chart_bottom - int(ratio * chart_h)
        label = f"{v}%"
        draw.text((chart_right + 6, y - 8), label, fill="#555555", font=axis_font)

    day_count = len(current_values)
    if day_count <= 1:
        return

    def x_for_day(i: int) -> int:
        return chart_left + int(i / (day_count - 1) * chart_w)

    def y_for_volume(v: Decimal) -> int:
        if y_max <= 0:
            return chart_bottom
        return chart_bottom - int(float(v) / y_max * chart_h)

    def y_for_lfl(v: Decimal) -> int:
        if lfl_high == lfl_low:
            return chart_bottom
        return chart_bottom - int((float(v) - lfl_low) / (lfl_high - lfl_low) * chart_h)

    current_points = [(x_for_day(i), y_for_volume(v)) for i, v in enumerate(current_values)]
    previous_points = [(x_for_day(i), y_for_volume(v)) for i, v in enumerate(previous_values)]
    lfl_points = [(x_for_day(i), y_for_lfl(v)) for i, v in enumerate(lfl_values)]

    draw.line(current_points, fill="#6f76ff", width=3)
    draw.line(previous_points, fill="#f08ad9", width=3)
    for x, y in lfl_points:
        draw.ellipse((x - 3, y - 3, x + 3, y + 3), fill="#b000c9", outline="#b000c9")

    # Match reference style: key date ticks like "3月1日 3月8日 3月16日 3月23日 3月31日".
    key_days = [1, 8, 16, 23, day_count]
    # keep order and de-duplicate for shorter months
    dedup_days: list[int] = []
    for d in key_days:
        if 1 <= d <= day_count and d not in dedup_days:
            dedup_days.append(d)
    for day in dedup_days:
        i = day - 1
        label = f"{month}月{day}日"
        x = x_for_day(i)
        y = chart_bottom + 8
        tw, _ = _measure(draw, label, axis_font)
        draw.text((x - tw // 2, y), label, fill="#666666", font=axis_font)


def render_dual_chart(
    *,
    output_path: Path,
    month: int,
    report_year: int,
    compare_year: int,
    current_free: list[Decimal],
    prev_free: list[Decimal],
    current_paid: list[Decimal],
    prev_paid: list[Decimal],
) -> None:
    img = Image.new("RGB", (1900, 360), "#e7e7e7")
    draw = ImageDraw.Draw(img)

    free_lfl = lfl_series(current_free, prev_free)
    paid_lfl = lfl_series(current_paid, prev_paid)

    _draw_panel(
        draw,
        panel_box=(20, 6, 940, 350),
        title="Organic Traffic LFL",
        current_label=f"{str(report_year)[-2:]} Organic Traffic",
        previous_label=f"{str(compare_year)[-2:]} Organic Traffic",
        current_values=current_free,
        previous_values=prev_free,
        lfl_values=free_lfl,
        month=month,
    )
    _draw_panel(
        draw,
        panel_box=(960, 6, 1880, 350),
        title="Paid Traffic LFL",
        current_label=f"{str(report_year)[-2:]} Paid Traffic",
        previous_label=f"{str(compare_year)[-2:]} Paid Traffic",
        current_values=current_paid,
        previous_values=prev_paid,
        lfl_values=paid_lfl,
        month=month,
    )

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


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Generate daily free/paid traffic LFL chart from cep_site_traffic_entry.")
    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],
    *,
    report_month: str = "",
    report_year: int = 0,
    compare_year: int = 0,
) -> dict[str, str]:
    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))

    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:
        current = fetch_month_uv_series(conn, year=report_year, month=month_num)
        previous = fetch_month_uv_series(conn, year=compare_year, month=month_num)
    finally:
        conn.close()

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

    render_dual_chart(
        output_path=image_path,
        month=month_num,
        report_year=report_year,
        compare_year=compare_year,
        current_free=current["free"],
        prev_free=previous["free"],
        current_paid=current["paid"],
        prev_paid=previous["paid"],
    )

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

    summary = {
        "report_period": {
            "month_label": month_label,
            "month_number": month_num,
            "year": report_year,
            "compare_year": compare_year,
        },
        "series": {
            "current_free": [int(v) for v in current["free"]],
            "previous_free": [int(v) for v in previous["free"]],
            "current_paid": [int(v) for v in current["paid"]],
            "previous_paid": [int(v) for v in previous["paid"]],
            "lfl_free_pct": [
                float(v.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
                for v in lfl_series(current["free"], previous["free"])
            ],
            "lfl_paid_pct": [
                float(v.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
                for v in lfl_series(current["paid"], previous["paid"])
            ],
        },
        "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(summary, 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,
        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()
