from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
from datetime import date
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/#/vis/datas/realTimeSells"
DEFAULT_DOWNLOAD_DIR = r"C:\vipdata"


@dataclass(frozen=True)
class MonthWindow:
    ym: str
    start: str
    end: str
    prefix: str


def run_cmd(
    args: list[str],
    *,
    cwd: Path,
    timeout: int = 180,
    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 = 180) -> 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_month: Any) -> int:
    if raw_month is None:
        return 1
    if isinstance(raw_month, int):
        return min(12, max(1, raw_month))
    text = str(raw_month).strip()
    match = re.search(r"(1[0-2]|0?[1-9])", text)
    if match:
        return int(match.group(1))
    return 1


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


def build_two_month_windows(year: int, month: int) -> list[MonthWindow]:
    curr = date(year, month, 1)
    prev_year = curr.year if curr.month > 1 else curr.year - 1
    prev_month = curr.month - 1 if curr.month > 1 else 12
    prev = date(prev_year, prev_month, 1)

    def build_window(d: date) -> MonthWindow:
        if d.month == 12:
            last_day = 31
        else:
            next_month = date(d.year + (1 if d.month == 12 else 0), 1 if d.month == 12 else d.month + 1, 1)
            last_day = (next_month - date.resolution).day
        ym = f"{d.year}-{d.month:02d}"
        return MonthWindow(
            ym=ym,
            start=f"{d.year}-{d.month:02d}-01",
            end=f"{d.year}-{d.month:02d}-{last_day:02d}",
            prefix=f"{d.year}{d.month:02d}",
        )

    return [build_window(curr), build_window(prev)]


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 currentUrl = page.url();
  const isTargetReady = currentUrl.includes('/realTimeSells') && !currentUrl.includes('/login.php');
  if (!(spec.no_navigate && isTargetReady)) {{
    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', ym: spec.ym, url: page.url() }};
  }}

  const frame = page.frames().find((f) => f.url().includes('vlp-admin.vip.com/vis/#/vis/datas/realTimeSells'));
  const ctx = frame || page;

  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', ym: spec.ym, url: page.url(), 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 setDateResult = await ctx.evaluate(() => {{
    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.el-range-input').length >= 2);
    if (!editor) return {{ ok: false, mode: 'no-daterange-editor' }};
    const vals = Array.from(editor.querySelectorAll('input.el-range-input')).map((i) => i.value || '');
    return {{ ok: true, mode: 'daterange-editor', values: vals }};
  }}).catch(() => ({{ ok: false, mode: 'inspect-failed' }}));

  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.el-range-input').length >= 2);
      if (!editor) return {{ ok: false, why: 'no-editor' }};
      const er = editor.getBoundingClientRect();
      const buttons = Array.from(document.querySelectorAll('button')).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 - 20 && b.y <= er.y + 140 && 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) }}));
  }};

  const queryRequests = [];
  const onReq = (req) => {{
    try {{
      const u = req.url();
      if (!(u.includes('/vis/') || u.includes('realTimeSells') || u.includes('vlp-admin.vip.com'))) return;
      const p = req.postData() || '';
      queryRequests.push({{ m: req.method(), u: u.slice(0, 500), p: p.slice(0, 4000) }});
    }} catch {{}}
  }};
  page.on('request', onReq);
  const queryClick = await clickNearDateButton(['\\u67e5\\u8be2']);
  await sleep(2200);
  page.off('request', onReq);

  const payloadMatched = queryRequests.some((r) =>
    ((r.p || '').includes(spec.start) && (r.p || '').includes(spec.end)) ||
    ((r.u || '').includes(spec.start) && (r.u || '').includes(spec.end))
  );
  if (!payloadMatched) {{
    return {{
      ok: false,
      code: 'QUERY_PAYLOAD_NOT_MATCHED',
      ym: spec.ym,
      setDateResult,
      queryClick,
      requests: queryRequests.slice(-5),
      url: page.url(),
      frame: frame ? frame.url() : null
    }};
  }}

  const downloadClick = await clickNearDateButton(['\\u4e0b\\u8f7d', '\\u5bfc\\u51fa']);
  if (!downloadClick.ok) {{
    return {{
      ok: false,
      code: 'DOWNLOAD_BUTTON_NOT_FOUND',
      ym: spec.ym,
      setDateResult,
      queryClick,
      downloadClick,
      url: page.url(),
      frame: frame ? frame.url() : null
    }};
  }}

  const dl = await page.waitForEvent('download', {{ timeout: 120000 }});
  const suggested = dl.suggestedFilename();
  const finalName = safeName(`${{spec.prefix}}_${{suggested}}`);
  const targetPath = `${{spec.download_dir.replace(/\\\\/g, '/')}}/${{finalName}}`;
  await dl.saveAs(targetPath);

  return {{
    ok: true,
    ym: spec.ym,
    prefix: spec.prefix,
    dateSetMode,
    setDateResult,
    queryClick,
    downloadClick,
    suggestedFilename: suggested,
    savedAs: targetPath
  }};
}}
"""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Download VIS realTimeSells files for configured month and previous month.")
    add_config_source_args(parser, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    parser.add_argument("--report-month", default="")
    parser.add_argument("--report-year", type=int, default=0)
    parser.add_argument("--download-dir", default=DEFAULT_DOWNLOAD_DIR)
    parser.add_argument("--session", default=SESSION_NAME)
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    config = load_runtime_config(
        args,
        default_config=Path(args.config) if getattr(args, "config", "") else Path(r"D:\projects\skills\vip-report\config.yaml"),
    )
    year, month = resolve_period(args, config)
    month_windows = build_two_month_windows(year, month)

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

    downloaded: list[dict[str, Any]] = []
    for m in month_windows:
        script_path = write_js(
            workdir,
            f"tmp-vis-live-download-{m.prefix}-{time.time_ns()}.js",
            build_download_js(
                {
                    "url": TARGET_URL,
                    "ym": m.ym,
                    "start": m.start,
                    "end": m.end,
                    "prefix": m.prefix,
                    "download_dir": str(download_dir),
                    "no_navigate": True,
                }
            ),
        )
        try:
            output = run_code(args.session, script_path, cwd=workdir, timeout=420)
            if "### Error" in output:
                raise RuntimeError(f"{m.ym} run-code failed: {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"{m.ym} no JSON result: {output.strip()}")

            result = json.loads(json_line)
            if not result.get("ok", False):
                code = result.get("code", "UNKNOWN")
                raise RuntimeError(f"{m.ym} failed ({code}): {json.dumps(result, ensure_ascii=False)}")
            downloaded.append({"month": m.ym, "result": result})
        finally:
            try:
                script_path.unlink()
            except FileNotFoundError:
                pass

    print(json.dumps({"ok": True, "downloads": downloaded}, ensure_ascii=False))


if __name__ == "__main__":
    main()
