from __future__ import annotations

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

import yaml
from tableau_export import normalize_image_size


SUPPORTED_SLIDES = {"S02", "S03", "S04", "S05", "S06", "S07", "S08", "S09", "S10", "S13"}
MONTHLY_SALES_SLIDES = {"S02", "S03"}
INVENTORY_MONTHLY_SLIDES = {"S04", "S05", "S06", "S07", "S08"}
TOP_PRODUCTS_SLIDES = {"S09", "S10"}
WAREHOUSE_SLIDES = {"S13"}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Re-crop already-downloaded Tableau images and render one slide operation set."
    )
    parser.add_argument(
        "--config",
        default=str((Path(__file__).resolve().parents[1] / "config.yaml")),
        help="Path to config.yaml",
    )
    parser.add_argument("--slide", required=True, help="Single slide code, for example S02")
    parser.add_argument("--report-month", required=True, help="Report month, for example 三月 / 3 / 03")
    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")
    parser.add_argument(
        "--month-mode",
        choices=["cumulative", "single"],
        default="cumulative",
        help="Monthly-sales month mode, default cumulative",
    )
    return parser.parse_args()


def load_config(config_path: Path) -> dict[str, Any]:
    if not config_path.exists():
        raise FileNotFoundError(f"Config not found: {config_path}")
    return yaml.safe_load(config_path.read_text(encoding="utf-8"))


def normalize_slide(slide: str) -> str:
    value = slide.strip().upper()
    if not value.startswith("S"):
        value = f"S{int(value):02d}"
    if value not in SUPPORTED_SLIDES:
        raise ValueError(f"Unsupported slide: {slide}. Supported: {','.join(sorted(SUPPORTED_SLIDES))}")
    return value


def resolve_years(args: argparse.Namespace, config: dict[str, Any]) -> tuple[int, int]:
    report_cfg = config.get("report", {})
    report_year = int(args.report_year or report_cfg.get("year", 2026))
    compare_year = int(args.compare_year or report_cfg.get("compare_year", report_year - 1))
    return report_year, compare_year


def load_manifest(manifest_path: Path) -> dict[str, Any]:
    if not manifest_path.exists():
        return {}
    return json.loads(manifest_path.read_text(encoding="utf-8"))


def build_raw_index(manifest: dict[str, Any]) -> dict[str, Path]:
    raw_by_capture: dict[str, Path] = {}

    for capture in manifest.get("source", {}).get("captures", []):
        capture_id = str(capture.get("capture_id", "")).strip()
        raw_path = capture.get("raw_screenshot")
        if not capture_id or not raw_path:
            continue
        resolved = Path(raw_path)
        if resolved.exists():
            raw_by_capture[capture_id] = resolved

    for asset in manifest.get("assets", []):
        capture_id = str(asset.get("source_capture_id") or asset.get("capture_id") or "").strip()
        raw_path = asset.get("raw_screenshot")
        if not capture_id or not raw_path:
            continue
        resolved = Path(raw_path)
        if resolved.exists():
            raw_by_capture[capture_id] = resolved

    return raw_by_capture


def resolve_raw_path(
    *,
    capture_id: str,
    captures_by_id: dict[str, dict[str, Any]],
    raw_by_capture: dict[str, Path],
    workdir: Path,
) -> Path:
    if capture_id in raw_by_capture and raw_by_capture[capture_id].exists():
        return raw_by_capture[capture_id]

    capture = captures_by_id.get(capture_id)
    if capture:
        candidate = (workdir / capture["raw_screenshot_name"]).resolve()
        if candidate.exists():
            raw_by_capture[capture_id] = candidate
            return candidate

    raise FileNotFoundError(
        f"Raw screenshot not found for capture_id={capture_id}. "
        "Please run Tableau sync for this slide once first."
    )


def normalize_s04_overall_raw_if_needed(
    *,
    slide: str,
    capture_id: str,
    source_path: Path,
    capture_spec: dict[str, Any] | None,
) -> Path:
    if slide != "S04" or capture_id != "overall" or not capture_spec:
        return source_path
    normalize_size = capture_spec.get("normalize_download_size")
    if not normalize_size:
        return source_path
    normalize_image_size(source_path, normalize_size)
    return source_path


def append_replace_image(
    operations: dict[str, list[Any]],
    *,
    slide: int,
    shape_id: int,
    shape_name: str,
    image_path: Path,
) -> None:
    operations["replace_images"].append(
        {
            "slide": slide,
            "shape_id": shape_id,
            "shape_name": shape_name,
            "image_path": str(image_path),
        }
    )


def recrop_monthly_sales(
    *,
    slide: str,
    report_month: str,
    report_year: int,
    compare_year: int,
    month_mode: str,
    workdir: Path,
    raw_by_capture: dict[str, Path],
    operations: dict[str, list[Any]],
) -> None:
    import sync_monthly_sales_assets as monthly_sales

    specs = monthly_sales.build_specs(report_month, report_year, compare_year, month_mode)
    captures_by_id = {item["capture_id"]: item for item in specs["captures"]}
    filtered_assets = [item for item in specs["assets"] if item["slide_code"] == slide]
    asset_dir = workdir / "assets" / "monthly-sales"
    asset_dir.mkdir(parents=True, exist_ok=True)

    for asset in filtered_assets:
        source_path = resolve_raw_path(
            capture_id=asset["capture_id"],
            captures_by_id=captures_by_id,
            raw_by_capture=raw_by_capture,
            workdir=workdir,
        )
        target_path = asset_dir / f"{asset['asset_name']}.png"
        if "composite" in asset:
            monthly_sales.compose_panels(source_path, target_path, asset["composite"])
        else:
            monthly_sales.crop_image(
                source_path,
                target_path,
                asset["crop"],
                resize_to=asset.get("resize_to"),
            )
        append_replace_image(
            operations,
            slide=asset["slide"],
            shape_id=asset["shape_id"],
            shape_name=asset["shape_name"],
            image_path=target_path,
        )


def recrop_inventory_monthly(
    *,
    slide: str,
    report_month: str,
    report_year: int,
    compare_year: int,
    workdir: Path,
    raw_by_capture: dict[str, Path],
    operations: dict[str, list[Any]],
) -> None:
    import sync_inventory_monthly_assets as inventory

    specs = inventory.build_specs(report_month, report_year, compare_year)
    captures_by_id = {item["capture_id"]: item for item in specs["captures"]}
    filtered_assets = [item for item in specs["assets"] if item["slide_code"] == slide]
    asset_dir = workdir / "assets" / "inventory-monthly"
    asset_dir.mkdir(parents=True, exist_ok=True)

    for asset in filtered_assets:
        source_path = resolve_raw_path(
            capture_id=asset["capture_id"],
            captures_by_id=captures_by_id,
            raw_by_capture=raw_by_capture,
            workdir=workdir,
        )
        source_path = normalize_s04_overall_raw_if_needed(
            slide=slide,
            capture_id=asset["capture_id"],
            source_path=source_path,
            capture_spec=captures_by_id.get(asset["capture_id"]),
        )
        target_path = asset_dir / f"{asset['asset_name']}.png"
        inventory.crop_image(
            source_path,
            target_path,
            asset["crop"],
            resize_to=asset.get("resize_to"),
        )
        append_replace_image(
            operations,
            slide=asset["slide"],
            shape_id=asset["shape_id"],
            shape_name=asset["shape_name"],
            image_path=target_path,
        )


def recrop_warehouse(
    *,
    slide: str,
    report_month: str,
    report_year: int,
    compare_year: int,
    workdir: Path,
    raw_by_capture: dict[str, Path],
    operations: dict[str, list[Any]],
) -> None:
    import sync_warehouse_100060_assets as warehouse

    month_label = warehouse.normalize_month_label(report_month)
    month_number = warehouse.month_label_to_number(month_label)
    specs = warehouse.build_specs(month_label, month_number, report_year, compare_year)
    captures_by_id = {item["capture_id"]: item for item in specs["captures"]}
    filtered_assets = [item for item in specs["assets"] if item["slide_code"] == slide]
    asset_dir = workdir / "assets" / "warehouse-100060"
    asset_dir.mkdir(parents=True, exist_ok=True)

    for asset in filtered_assets:
        source_path = resolve_raw_path(
            capture_id=asset["capture_id"],
            captures_by_id=captures_by_id,
            raw_by_capture=raw_by_capture,
            workdir=workdir,
        )
        target_path = asset_dir / f"{asset['asset_name']}.png"
        warehouse.crop_image(
            source_path,
            target_path,
            asset["crop"],
            resize_to=asset.get("resize_to"),
        )
        append_replace_image(
            operations,
            slide=asset["slide"],
            shape_id=asset["shape_id"],
            shape_name=asset["shape_name"],
            image_path=target_path,
        )


def recrop_top_products(
    *,
    slide: str,
    workdir: Path,
    manifest: dict[str, Any],
    raw_by_capture: dict[str, Path],
    operations: dict[str, list[Any]],
) -> None:
    import sync_top_products_assets as top_products

    manifest_assets = [
        item for item in manifest.get("assets", []) if str(item.get("slide_code", "")).upper() == slide
    ]
    if not manifest_assets:
        raise FileNotFoundError(
            f"No existing top-products manifest assets for {slide}. "
            "Please run top-products Tableau sync once first."
        )

    chart_spec = top_products.SLIDE_SPECS[slide]
    captures_by_id = {chart_spec["capture_id"]: {"raw_screenshot_name": chart_spec["raw_screenshot_name"]}}
    chart_source = resolve_raw_path(
        capture_id=chart_spec["capture_id"],
        captures_by_id=captures_by_id,
        raw_by_capture=raw_by_capture,
        workdir=workdir,
    )

    for asset in manifest_assets:
        target_path = Path(asset["asset_path"]).resolve()
        target_path.parent.mkdir(parents=True, exist_ok=True)

        if asset.get("asset_name") == chart_spec["asset_name"]:
            top_products.crop_image(
                chart_source,
                target_path,
                chart_spec["crop"],
                resize_to=chart_spec.get("resize_to"),
            )
        elif not target_path.exists():
            raise FileNotFoundError(
                f"Top-products dependent asset is missing: {target_path}. "
                "Please run top-products Tableau sync once first."
            )

        append_replace_image(
            operations,
            slide=int(asset["slide"]),
            shape_id=int(asset["shape_id"]),
            shape_name=str(asset["shape_name"]),
            image_path=target_path,
        )


def main() -> None:
    args = parse_args()
    slide = normalize_slide(args.slide)

    script_path = Path(__file__).resolve()
    scripts_dir = script_path.parent
    if str(scripts_dir) not in sys.path:
        sys.path.insert(0, str(scripts_dir))

    config_path = Path(args.config).resolve()
    config = load_config(config_path)
    report_year, compare_year = resolve_years(args, config)

    workdir = Path(config["paths"]["workdir"])
    if not workdir.is_absolute():
        workdir = (config_path.parent / workdir).resolve()

    manifest_path_map = {
        "monthly-sales": workdir / "data" / "monthly-sales" / "monthly-sales-assets.live.json",
        "inventory-monthly": workdir / "data" / "inventory-monthly" / "inventory-monthly-assets.live.json",
        "top-products": workdir / "data" / "top-products" / "top-products-assets.live.json",
        "warehouse-100060": workdir / "data" / "warehouse-100060" / "warehouse-100060-assets.live.json",
    }

    if slide in MONTHLY_SALES_SLIDES:
        manifest = load_manifest(manifest_path_map["monthly-sales"])
    elif slide in INVENTORY_MONTHLY_SLIDES:
        manifest = load_manifest(manifest_path_map["inventory-monthly"])
    elif slide in TOP_PRODUCTS_SLIDES:
        manifest = load_manifest(manifest_path_map["top-products"])
    elif slide in WAREHOUSE_SLIDES:
        manifest = load_manifest(manifest_path_map["warehouse-100060"])
    else:
        raise ValueError(f"Unsupported slide: {slide}")

    raw_by_capture = build_raw_index(manifest)
    operations: dict[str, list[Any]] = {
        "replace_text": [],
        "replace_tables": [],
        "replace_charts": [],
        "replace_images": [],
    }

    if slide in MONTHLY_SALES_SLIDES:
        recrop_monthly_sales(
            slide=slide,
            report_month=args.report_month,
            report_year=report_year,
            compare_year=compare_year,
            month_mode=args.month_mode,
            workdir=workdir,
            raw_by_capture=raw_by_capture,
            operations=operations,
        )
    elif slide in INVENTORY_MONTHLY_SLIDES:
        recrop_inventory_monthly(
            slide=slide,
            report_month=args.report_month,
            report_year=report_year,
            compare_year=compare_year,
            workdir=workdir,
            raw_by_capture=raw_by_capture,
            operations=operations,
        )
    elif slide in TOP_PRODUCTS_SLIDES:
        recrop_top_products(
            slide=slide,
            workdir=workdir,
            manifest=manifest,
            raw_by_capture=raw_by_capture,
            operations=operations,
        )
    elif slide in WAREHOUSE_SLIDES:
        recrop_warehouse(
            slide=slide,
            report_month=args.report_month,
            report_year=report_year,
            compare_year=compare_year,
            workdir=workdir,
            raw_by_capture=raw_by_capture,
            operations=operations,
        )

    output_ops_path = workdir / f"render-ops.recrop.{slide.lower()}.json"
    output_ops_path.write_text(json.dumps(operations, ensure_ascii=False, indent=2), encoding="utf-8")
    print(str(output_ops_path))


if __name__ == "__main__":
    main()
