from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import time
from pathlib import Path
from typing import Any

import yaml


NPX_EXECUTABLE = shutil.which("npx.cmd") or shutil.which("npx") or "npx"
PLAYWRIGHT_CMD = [NPX_EXECUTABLE, "--yes", "--package", "@playwright/cli", "playwright-cli"]
SESSION_NAME = "vip-report-monthly-sales"
TARGET_URL = "https://compass.vip.com/frontend/index.html#/rejectionAnalysis/index"
DOWNLOAD_CENTER_URL = "https://compass.vip.com/frontend/index.html#/dataCapturing/downloadCenter"
DEFAULT_DOWNLOAD_DIR = r"C:\vipdata"


def run_cmd(args: list[str], *, cwd: Path, timeout: int = 300) -> subprocess.CompletedProcess[str]:
    env = os.environ.copy()
    npm_cache = cwd / ".npm-cache"
    npm_cache.mkdir(parents=True, exist_ok=True)
    env["NPM_CONFIG_CACHE"] = str(npm_cache)
    env["npm_config_cache"] = str(npm_cache)
    return subprocess.run(
        args,
        cwd=str(cwd),
        text=True,
        encoding="utf-8",
        errors="replace",
        capture_output=True,
        timeout=timeout,
        check=True,
        env=env,
    )


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


def resolve_period(config_path: Path, report_year_arg: int, report_month_arg: str) -> tuple[int, int]:
    cfg = yaml.safe_load(config_path.read_text(encoding="utf-8"))
    report_cfg = cfg.get("report", {})
    year = int(report_year_arg or report_cfg.get("year", 2026))
    month = parse_month_number(report_month_arg or report_cfg.get("month_cn", "1"))
    return year, month


def build_js(spec: dict[str, Any]) -> str:
    payload = json.dumps(spec, ensure_ascii=False)
    return f"""async function(page) {{
  const spec = {payload};
  const sleep = (ms) => page.waitForTimeout(ms);
  const safeName = (name) => String(name || '')
    .replace(/[\\\\/:*?"<>|]/g, '_')
    .replace(/\\s+/g, '_')
    .replace(/_+/g, '_');

  await page.goto(`${{spec.url}}?t=${{Date.now()}}`, {{ waitUntil: 'domcontentloaded', timeout: 120000 }});
  await sleep(5000);

  const tab = page.locator('.el-tabs__item:has-text("拒退商品分析")').first();
  if (!(await tab.count().catch(() => 0))) {{
    return {{ ok: false, code: 'GOODS_TAB_NOT_FOUND', url: page.url() }};
  }}
  await tab.click({{ timeout: 5000, force: true }});
  await sleep(2500);

  const activeText = await page.evaluate(() => {{
    const active = document.querySelector('.el-tabs__item.is-active');
    return (active?.textContent || '').replace(/\\s+/g, ' ').trim();
  }}).catch(() => '');
  if (!activeText.includes('拒退商品分析')) {{
    return {{ ok: false, code: 'GOODS_TAB_NOT_ACTIVE', activeText, url: page.url() }};
  }}

  const downloadBtn = page.locator('button:has-text("下载数据")').first();
  if (!(await downloadBtn.count().catch(() => 0))) {{
    return {{ ok: false, code: 'DOWNLOAD_DATA_BUTTON_NOT_FOUND', activeText, url: page.url() }};
  }}
  await downloadBtn.click({{ timeout: 5000, force: true }});
  await sleep(700);

  const noNeed = page.locator('button:has-text("不需要"), span:has-text("不需要")').first();
  if (await noNeed.count().catch(() => 0)) {{
    await noNeed.click({{ timeout: 3000, force: true }}).catch(() => null);
    await sleep(700);
  }}

  await page.goto(spec.download_center_url, {{ waitUntil: 'domcontentloaded', timeout: 120000 }});
  await sleep(2000);

  const rowsSeen = [];
  for (let attempt = 0; attempt < spec.max_retries; attempt++) {{
    const queryBtn = page.locator('button:has-text("查询"), span:has-text("查询")').first();
    if (await queryBtn.count().catch(() => 0)) {{
      await queryBtn.click({{ timeout: 2500 }}).catch(() => null);
    }}
    await sleep(2200);

    const rowCount = await page.locator('tbody tr').count().catch(() => 0);
    for (let idx = 0; idx < Math.min(rowCount, 10); idx++) {{
      const row = page.locator('tbody tr').nth(idx);
      const text = (await row.innerText().catch(() => '')).replace(/\\s+/g, ' ').trim();
      const nameEl = row.locator('.status-name-span').first();
      let fileName = await nameEl.getAttribute('title').catch(() => '');
      if (!fileName) fileName = (await nameEl.innerText().catch(() => '')).trim();
      rowsSeen.push({{ row: idx + 1, fileName, text }});
      if (!fileName.includes('款号粒度') || !/执行成功|成功/.test(text)) continue;

      const btn = row.locator('button:has-text("下载"), span:has-text("下载")').first();
      if (!(await btn.count().catch(() => 0))) continue;
      const [dl] = await Promise.all([
        page.waitForEvent('download', {{ timeout: 120000 }}),
        btn.click({{ timeout: 5000, force: true }}),
      ]);
      const suggested = dl.suggestedFilename();
      const finalName = safeName(`${{spec.prefix}}_${{fileName || suggested}}`);
      const targetPath = `${{spec.download_dir.replace(/\\\\/g, '/')}}/${{finalName}}`;
      await dl.saveAs(targetPath);
      return {{ ok: true, savedAs: targetPath, fileName, suggestedFilename: suggested, rowText: text }};
    }}
  }}
  return {{ ok: false, code: 'GOODS_STYLE_FILE_NOT_READY', rowsSeen: rowsSeen.slice(0, 30), url: page.url() }};
}}
"""


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Download Compass rejection goods style-granularity file.")
    p.add_argument("--config", default=r"D:\projects\skills\vip-report\config.yaml")
    p.add_argument("--session", default=SESSION_NAME)
    p.add_argument("--report-year", type=int, default=0)
    p.add_argument("--report-month", default="")
    p.add_argument("--download-dir", default=DEFAULT_DOWNLOAD_DIR)
    p.add_argument("--max-retries", type=int, default=30)
    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"))
    workdir = Path(cfg["paths"]["workdir"]).resolve()
    year, month = resolve_period(config_path, args.report_year, args.report_month)
    prefix = f"{year}{month:02d}"
    download_dir = Path(args.download_dir).resolve()
    download_dir.mkdir(parents=True, exist_ok=True)

    js_path = workdir / f"tmp-compass-rejection-goods-style-download-{prefix}-{time.time_ns()}.js"
    js_path.write_text(
        build_js(
            {
                "url": TARGET_URL,
                "download_center_url": DOWNLOAD_CENTER_URL,
                "prefix": prefix,
                "download_dir": str(download_dir),
                "max_retries": int(args.max_retries),
            }
        ),
        encoding="utf-8",
    )
    try:
        out = run_cmd(
            PLAYWRIGHT_CMD + ["--session", args.session, "run-code", "--filename", str(js_path)],
            cwd=workdir,
            timeout=1200,
        ).stdout
    finally:
        js_path.unlink(missing_ok=True)

    if "### Error" in out:
        raise RuntimeError(out.strip())
    json_line = ""
    for line in out.splitlines():
        s = line.strip()
        if s.startswith("{") and s.endswith("}"):
            json_line = s
            break
    if not json_line:
        raise RuntimeError(f"No JSON result from run-code: {out}")
    result = json.loads(json_line)
    if not result.get("ok", False):
        raise RuntimeError(json.dumps(result, ensure_ascii=False))
    print(json.dumps(result, ensure_ascii=False))


if __name__ == "__main__":
    main()
