from __future__ import annotations

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

import yaml

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


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://vis.vip.com/#/app-i/vlp/vis/#/datas/real-time-sells-vendor"
DEFAULT_DOWNLOAD_DIR = r"C:\vipdata"


def run_cmd(
    args: list[str],
    *,
    cwd: Path,
    timeout: int = 300,
    check: bool = True,
) -> 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)
    return subprocess.run(
        args,
        cwd=str(cwd),
        text=True,
        encoding="utf-8",
        errors="replace",
        capture_output=True,
        timeout=timeout,
        check=check,
        env=env,
    )


def run_playwright(args: list[str], *, cwd: Path, timeout: int = 300) -> str:
    result = run_cmd(PLAYWRIGHT_CMD + args, cwd=cwd, timeout=timeout)
    return result.stdout


def write_js(workdir: Path, name: str, content: str) -> Path:
    path = workdir / name
    path.write_text(content, encoding="utf-8")
    return path


def run_code(session: str, script_path: Path, *, cwd: Path, timeout: int = 420) -> str:
    return run_playwright(["--session", session, "run-code", "--filename", str(script_path)], cwd=cwd, timeout=timeout)


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(cfg: dict[str, Any], report_year_arg: int, report_month_arg: str) -> tuple[int, int]:
    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 month_range(year: int, month: int) -> tuple[str, str, str]:
    _, last_day = calendar.monthrange(year, month)
    start = f"{year}-{month:02d}-01"
    end = f"{year}-{month:02d}-{last_day:02d}"
    prefix = f"{year}{month:02d}"
    return start, end, prefix


def build_download_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);

  function safeName(name) {{
    return String(name || '')
      .replace(/[\\\\/:*?"<>|]/g, '_')
      .replace(/\\s+/g, '_')
      .replace(/_+/g, '_');
  }}

  const current = page.url();
  const inTarget = current.includes('/real-time-sells-vendor') && !current.includes('/login.php');
  if (!(spec.no_navigate && inTarget)) {{
    await page.goto(spec.url, {{ waitUntil: 'domcontentloaded', timeout: 120000 }});
    await sleep(2500);
  }} else {{
    await sleep(800);
  }}

  if (page.url().includes('/login.php') || page.url().includes('/passport.vip.com/')) {{
    return {{ ok: false, code: 'LOGIN_REQUIRED', url: page.url() }};
  }}

  let frame = null;
  for (let i = 0; i < 20; i++) {{
    frame = page.frames().find((f) => f.url().includes('vlp-admin.vip.com') && f.url().includes('/real-time-sells-vendor'));
    if (frame) break;
    await sleep(500);
  }}
  const ctx = frame || page;

  // 商品效果 tab
  const tabClicked = await ctx.evaluate(() => {{
    const visible = (el) => !!el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden';
    const cands = Array.from(document.querySelectorAll('div,span,button,a')).filter(visible);
    const target = cands.find((el) => (el.textContent || '').trim().includes('\\u5546\\u54c1\\u6548\\u679c'));
    if (target) {{
      target.click();
      return true;
    }}
    return false;
  }}).catch(() => false);
  await sleep(700);

  const midTabClick = await ctx.evaluate(() => {{
    const visible = (el) => !!el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden';
    const text = (el) => (el.textContent || '').replace(/\\s+/g, ' ').trim();
    const cands = Array.from(document.querySelectorAll('.el-tabs__item, div, span, a, button'))
      .filter(visible)
      .map((el) => ({{ el, t: text(el) }}))
      .filter((x) => /商品效果\\s*\\(\\s*MID\\s*\\)/i.test(x.t) || /商品效果.*MID/i.test(x.t) || /^MID$/i.test(x.t));
    if (!cands.length) return {{ ok: false, why: 'mid-tab-not-found' }};
    cands.sort((a, b) => a.t.length - b.t.length);
    cands[0].el.click();
    return {{ ok: true, text: cands[0].t }};
  }}).catch((e) => ({{ ok: false, why: String(e) }}));
  await sleep(900);

  let dateSetMode = 'placeholder-inputs';
  let startInput = ctx.locator('input[placeholder="\\u5f00\\u59cb\\u65e5\\u671f"]').first();
  let endInput = ctx.locator('input[placeholder="\\u7ed3\\u675f\\u65e5\\u671f"]').first();
  let startCount = await startInput.count().catch(() => 0);
  let endCount = await endInput.count().catch(() => 0);
  if (!startCount || !endCount) {{
    dateSetMode = 'range-editor-inputs';
    startInput = ctx.locator('.el-date-editor--daterange input.el-range-input, .el-range-editor input.el-range-input').nth(0);
    endInput = ctx.locator('.el-date-editor--daterange input.el-range-input, .el-range-editor input.el-range-input').nth(1);
    startCount = await startInput.count().catch(() => 0);
    endCount = await endInput.count().catch(() => 0);
  }}
  if (!startCount || !endCount) {{
    return {{ ok: false, code: 'DATE_INPUT_NOT_FOUND', tabClicked, frame: frame ? frame.url() : null }};
  }}

  await startInput.click({{ clickCount: 3, timeout: 5000 }}).catch(() => null);
  await startInput.fill(spec.start, {{ timeout: 5000 }}).catch(() => null);
  await startInput.press('Enter').catch(() => null);
  await endInput.click({{ clickCount: 3, timeout: 5000 }}).catch(() => null);
  await endInput.fill(spec.end, {{ timeout: 5000 }}).catch(() => null);
  await endInput.press('Enter').catch(() => null);
  await ctx.locator('body').click({{ position: {{ x: 2, y: 2 }} }}).catch(() => null);
  await sleep(500);

  const clickNearDateButton = async (keywords) => {{
    return await ctx.evaluate((keys) => {{
      const visible = (el) => !!el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden';
      const editor = Array.from(document.querySelectorAll('.el-date-editor--daterange, .el-range-editor, .el-date-editor'))
        .find((el) => visible(el) && el.querySelectorAll('input').length >= 2);
      if (!editor) return {{ ok: false, why: 'no-editor' }};
      const er = editor.getBoundingClientRect();
      const buttons = Array.from(document.querySelectorAll('button,span,a')).filter(visible).map((b) => {{
        const r = b.getBoundingClientRect();
        return {{ el: b, text: (b.innerText || '').trim(), y: r.y }};
      }});
      const cands = buttons
        .filter((b) => b.y >= er.y - 30 && b.y <= er.y + 180 && keys.some((k) => b.text.includes(k)))
        .sort((a, b) => Math.abs(a.y - (er.y + 62)) - Math.abs(b.y - (er.y + 62)));
      if (!cands.length) return {{ ok: false, why: 'no-button-near-date', keys }};
      cands[0].el.click();
      return {{ ok: true, text: cands[0].text }};
    }}, keywords).catch((e) => ({{ ok: false, why: String(e) }}));
  }};

  let queryClick = await clickNearDateButton(['\\u67e5\\u8be2']);
  if (!queryClick.ok) {{
    const queryBtn = ctx.locator('button:has-text("\\u67e5\\u8be2")').first();
    if (await queryBtn.count().catch(() => 0)) {{
      await queryBtn.click({{ timeout: 5000 }}).catch(() => null);
      queryClick = {{ ok: true, text: '\\u67e5\\u8be2', mode: 'fallback-locator' }};
    }}
  }}
  await sleep(2000);

  const downloadClick = await ctx.evaluate(() => {{
    const visible = (el) => !!el && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden';
    const textOf = (el) => (el.textContent || '').replace(/\\s+/g, ' ').trim();
    const all = Array.from(document.querySelectorAll('*')).filter(visible);

    const midAnchors = all.filter((el) => /MID/i.test(textOf(el)));
    for (const a of midAnchors) {{
      let cur = a;
      for (let i = 0; i < 8 && cur; i++) {{
        const btns = Array.from(cur.querySelectorAll('button')).filter(visible);
        const dl = btns.filter((b) => /\\u4e0b\\u8f7d|\\u5bfc\\u51fa/.test(textOf(b)));
        if (dl.length) {{
          dl[0].click();
          return {{ ok: true, text: textOf(dl[0]), mode: 'near-mid' }};
        }}
        cur = cur.parentElement;
      }}
    }}

    const anchors = all.filter((el) => textOf(el).includes('\\u5546\\u54c1\\u6548\\u679c'));

    for (const a of anchors) {{
      let cur = a;
      for (let i = 0; i < 6 && cur; i++) {{
        const btns = Array.from(cur.querySelectorAll('button')).filter(visible);
        const dl = btns.filter((b) => /\\u4e0b\\u8f7d|\\u5bfc\\u51fa/.test(textOf(b)));
        if (dl.length) {{
          dl[0].click();
          return {{ ok: true, text: textOf(dl[0]), mode: 'near-goods-effect' }};
        }}
        cur = cur.parentElement;
      }}
    }}

    const fallback = Array.from(document.querySelectorAll('button')).filter(visible).find((b) => /\\u4e0b\\u8f7d|\\u5bfc\\u51fa/.test(textOf(b)));
    if (fallback) {{
      fallback.click();
      return {{ ok: true, text: textOf(fallback), mode: 'fallback-first-download' }};
    }}
    return {{ ok: false, why: 'download-not-found' }};
  }}).catch(() => ({{ ok: false, why: 'download-click-error' }}));
  if (!downloadClick.ok) {{
    return {{
      ok: false,
      code: 'DOWNLOAD_BUTTON_NOT_FOUND',
      tabClicked,
      midTabClick,
      queryClick,
      downloadClick,
      frame: frame ? frame.url() : null,
      url: page.url()
    }};
  }}

  const dl = await page.waitForEvent('download', {{ timeout: 120000 }});
  const suggested = dl.suggestedFilename();
  if (!/MID/i.test(suggested || '')) {{
    return {{
      ok: false,
      code: 'NOT_MID_FILE',
      suggestedFilename: suggested,
      tabClicked,
      midTabClick,
      queryClick,
      downloadClick
    }};
  }}
  const finalName = safeName(`${{spec.prefix}}_goods_effect_${{suggested}}`);
  const targetPath = `${{spec.download_dir.replace(/\\\\/g, '/')}}/${{finalName}}`;
  await dl.saveAs(targetPath);

  return {{
    ok: true,
    tabClicked,
    midTabClick,
    dateSetMode,
    queryClick,
    downloadClick,
    start: spec.start,
    end: spec.end,
    suggestedFilename: suggested,
    savedAs: targetPath
  }};
}}
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Download VIS vendor real-time sells goods-effect data.")
    add_config_source_args(parser, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    parser.add_argument("--download-dir", default=DEFAULT_DOWNLOAD_DIR)
    parser.add_argument("--session", default=SESSION_NAME)
    parser.add_argument("--report-year", type=int, default=0)
    parser.add_argument("--report-month", default="")
    parser.add_argument("--auto-load", action="store_true", default=True, help="Auto load downloaded file to MySQL.")
    parser.add_argument("--no-auto-load", action="store_false", dest="auto_load", help="Only download, skip MySQL load.")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    config_path = Path(args.config or r"D:\projects\skills\vip-report\config.yaml")
    cfg = load_runtime_config(args, default_config=config_path)
    year, month = resolve_report_month(cfg, args.report_year, args.report_month)
    start, end, prefix = month_range(year, month)

    workdir = Path(cfg["paths"]["workdir"]).resolve()
    download_dir = Path(args.download_dir).resolve()
    download_dir.mkdir(parents=True, exist_ok=True)

    script_path = write_js(
        workdir,
        f"tmp-vis-vendor-goods-effect-{prefix}-{time.time_ns()}.js",
        build_download_js(
            {
                "url": TARGET_URL,
                "start": start,
                "end": end,
                "prefix": prefix,
                "download_dir": str(download_dir),
                "no_navigate": True,
            }
        ),
    )
    try:
        output = run_code(args.session, script_path, cwd=workdir, timeout=420)
    finally:
        try:
            script_path.unlink()
        except FileNotFoundError:
            pass

    if "### Error" in output:
        raise RuntimeError(output.strip())

    json_line = ""
    for line in output.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: {output}")

    result = json.loads(json_line)
    if not result.get("ok", False):
        raise RuntimeError(json.dumps(result, ensure_ascii=False))

    if args.auto_load:
        loader_script = Path(__file__).resolve().parent / "load_vis_vendor_goods_effect_to_mysql.py"
        loader_args: list[str] = [
            sys.executable,
            str(loader_script),
            "--download-dir",
            str(download_dir),
            "--report-year",
            str(year),
            "--report-month",
            str(month),
            "--file",
            str(result.get("savedAs", "")),
        ]
        if args.config_source == "yaml":
            loader_args.extend(["--config", str(config_path.resolve())])
        else:
            loader_args.extend(
                [
                    "--config-source",
                    args.config_source,
                    "--config-table",
                    args.config_table,
                    "--config-key",
                    args.config_key,
                    "--mysql-host",
                    args.mysql_host,
                    "--mysql-port",
                    str(args.mysql_port),
                    "--mysql-user",
                    args.mysql_user,
                    "--mysql-password",
                    args.mysql_password,
                    "--mysql-database",
                    args.mysql_database,
                ]
            )
        load_proc = subprocess.run(
            loader_args,
            cwd=str(workdir),
            text=True,
            encoding="utf-8",
            errors="replace",
            capture_output=True,
            check=False,
        )
        if load_proc.returncode != 0:
            raise RuntimeError(
                "download ok, but auto-load failed: "
                f"code={load_proc.returncode}, stderr={load_proc.stderr.strip()}, stdout={load_proc.stdout.strip()}"
            )
        result["autoLoad"] = {"ok": True, "message": load_proc.stdout.strip()}

    output_text = json.dumps(result, ensure_ascii=False)
    try:
        print(output_text)
    except UnicodeEncodeError:
        print(json.dumps(result, ensure_ascii=True))


if __name__ == "__main__":
    main()
