from __future__ import annotations

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

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"
TRAFFIC_URL = "https://compass.vip.com/frontend/index.html#/traffic/trafficSource"
DOWNLOAD_CENTER_BASE = "https://compass.vip.com/frontend/index.html#/dataCapturing/downloadCenter?taskType="
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)
    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:
    return run_cmd(PLAYWRIGHT_CMD + args, cwd=cwd, timeout=timeout).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 = 1800) -> str:
    return run_playwright(["--session", session, "run-code", "--filename", str(script_path)], cwd=cwd, timeout=timeout)


def ensure_browser_session(session: str, *, cwd: Path) -> None:
    try:
        out = run_playwright(["--session", session, "tab-list"], cwd=cwd, timeout=30)
        if "### Result" in out:
            return
    except Exception:
        pass
    run_playwright(["--session", session, "open", TRAFFIC_URL], cwd=cwd, timeout=120)


def parse_month_number(raw: Any) -> int:
    if raw is None:
        return 1
    if isinstance(raw, int):
        return max(1, min(12, 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_period(cfg: dict[str, Any], report_year_arg: int, report_month_arg: str, compare_year_arg: int) -> tuple[int, int, int]:
    report_cfg = cfg.get("report", {})
    report_year = int(report_year_arg or report_cfg.get("year", 2026))
    report_month = parse_month_number(report_month_arg or report_cfg.get("month_cn", "1"))
    compare_year = int(compare_year_arg or report_cfg.get("compare_year", report_year - 1))
    return report_year, report_month, compare_year


def month_ym(year: int, month: int) -> str:
    return f"{year}-{month:02d}"


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 TRAFFIC=spec.traffic_url;
  const DC_BASE=spec.download_center_base;
  const SAVE=String(spec.download_dir||'C:/vipdata').replace(/\\\\/g,'/');
  const sleep=(ms)=>page.waitForTimeout(ms);

  async function setMonthAndQuery(ym) {{
    await page.goto(TRAFFIC,{{waitUntil:'domcontentloaded',timeout:90000}});
    await sleep(2200);
    const ret = await page.evaluate(async (targetYm) => {{
      const vis=(el)=>{{
        if(!el) return false;
        const cs = getComputedStyle(el);
        if(cs.display==='none' || cs.visibility==='hidden') return false;
        const r = el.getBoundingClientRect();
        return r.width > 2 && r.height > 2;
      }};
      const click=(el)=>{{ if(!el) return false; el.scrollIntoView({{block:'center'}}); el.click(); return true; }};
      const wait=(ms)=>new Promise(r=>setTimeout(r,ms));
      const targetYear = parseInt(targetYm.slice(0,4),10);
      const targetMonth = parseInt(targetYm.slice(5,7),10);
      const last = new Date(targetYear, targetMonth, 0).getDate();
      const start = `${{targetYear}}-${{String(targetMonth).padStart(2,'0')}}-01`;
      const end = `${{targetYear}}-${{String(targetMonth).padStart(2,'0')}}-${{String(last).padStart(2,'0')}}`;

      const dd = Array.from(document.querySelectorAll('span.el-dropdown-link.el-dropdown-selfdefine')).find(el=>vis(el));
      if(dd){{
        if((dd.textContent||'').trim() !== '月') {{
          click(dd); await wait(120);
          const m = Array.from(document.querySelectorAll('li.el-dropdown-menu__item')).find(li=>vis(li)&&(li.textContent||'').trim()==='月');
          if(m) {{ click(m); await wait(260); }}
        }}
      }}

      const monthInput = Array.from(document.querySelectorAll('input')).find(i=>vis(i)&&/^\\d{{4}}-\\d{{2}}$/.test((i.value||'').trim()));
      if(!monthInput){{
        const dateInputs = Array.from(document.querySelectorAll('input')).filter(i=>vis(i)&&/^\\d{{4}}-\\d{{2}}-\\d{{2}}$/.test((i.value||'').trim()));
        if(dateInputs.length >= 2){{
          dateInputs[0].removeAttribute('readonly');
          dateInputs[1].removeAttribute('readonly');
          dateInputs[0].value = start;
          dateInputs[1].value = end;
          dateInputs[0].dispatchEvent(new Event('input',{{bubbles:true}}));
          dateInputs[0].dispatchEvent(new Event('change',{{bubbles:true}}));
          dateInputs[1].dispatchEvent(new Event('input',{{bubbles:true}}));
          dateInputs[1].dispatchEvent(new Event('change',{{bubbles:true}}));
          const q2 = Array.from(document.querySelectorAll('button')).find(b=>vis(b)&&(b.innerText||'').includes('查询'));
          if(q2) q2.click();
          await wait(1400);
          return {{ok:true, mode:'date-range'}};
        }}
        return {{ok:false,step:'noMonthOrDateInputs'}};
      }}

      let panel=null;
      for(let i=0;i<8;i++){{
        monthInput.click();
        monthInput.dispatchEvent(new Event('focus',{{bubbles:true}}));
        await wait(150);
        panel = Array.from(document.querySelectorAll('.el-date-picker')).find(p=>vis(p));
        if(panel) break;
      }}
      if(!panel) return {{ok:false,step:'noPanel'}};

      const prev=panel.querySelector('button.el-icon-d-arrow-left');
      const next=panel.querySelector('button.el-icon-d-arrow-right');
      const yearVal=()=>parseInt((panel.querySelector('.el-date-picker__header-label')?.textContent||'').trim(),10);
      let y=yearVal();
      for(let i=0;i<30 && !Number.isNaN(y) && y>targetYear;i++){{ if(prev) prev.click(); await wait(120); y=yearVal(); }}
      for(let i=0;i<30 && !Number.isNaN(y) && y<targetYear;i++){{ if(next) next.click(); await wait(120); y=yearVal(); }}

      const monthLabels = ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'];
      const targetLabel = monthLabels[targetMonth - 1];
      const monthCell = Array.from(panel.querySelectorAll('.el-month-table td .cell'))
        .find(c => (c.textContent || '').trim() === targetLabel);
      if(!monthCell) return {{ok:false,step:'noTargetMonth', targetLabel}};
      monthCell.click(); await wait(260);

      const q = Array.from(document.querySelectorAll('button')).find(b=>vis(b)&&(b.innerText||'').includes('查询'));
      if(q) q.click();
      await wait(1400);
      return {{ok:true}};
    }}, ym);
    if(!ret.ok) throw new Error('setMonth fail: '+JSON.stringify(ret));
    await sleep(1500);
  }}

  function pickTaskId(data){{
    if(!data) return '';
    const keys = ['message','taskId','task_id','id'];
    for (const k of keys){{
      if (data[k] !== undefined && data[k] !== null && String(data[k]).trim()) return String(data[k]).trim();
    }}
    if (data.data && typeof data.data === 'object'){{
      for (const k of keys){{
        if (data.data[k] !== undefined && data.data[k] !== null && String(data.data[k]).trim()) return String(data.data[k]).trim();
      }}
    }}
    if (Array.isArray(data.data) && data.data.length > 0){{
      const x = data.data[0];
      if (x && typeof x === 'object'){{
        for (const k of keys){{
          if (x[k] !== undefined && x[k] !== null && String(x[k]).trim()) return String(x[k]).trim();
        }}
      }}
    }}
    return '';
  }}

  async function submitTopSummaryAndGetTaskId(){{
    const waitResp = page.waitForResponse(r=>r.url().includes('/operate/flow/flowEntrance/asyncExportFlowEntranceOverview') && r.request().method()==='POST',{{timeout:90000}});
    const ret = await page.evaluate(async ()=>{{
      const vis=(el)=>{{
        if(!el) return false;
        const cs = getComputedStyle(el);
        if(cs.display==='none' || cs.visibility==='hidden') return false;
        const r = el.getBoundingClientRect();
        return r.width > 2 && r.height > 2;
      }};
      const wait=(ms)=>new Promise(r=>setTimeout(r,ms));
      const exp = Array.from(document.querySelectorAll('button')).find(b=>vis(b)&&(b.innerText||'').includes('数据导出'));
      if(!exp) return {{ok:false,step:'noExport'}};
      exp.click(); await wait(200);
      const d = Array.from(document.querySelectorAll('.el-dialog__wrapper')).find(w=>vis(w)&&(w.innerText||'').includes('下载数据'));
      if(!d) return {{ok:false,step:'noDialog'}};
      const s = Array.from(d.querySelectorAll('span.el-radio-button__inner')).find(el=>(el.innerText||'').trim()==='汇总下载');
      if(!s) return {{ok:false,step:'noSummary'}};
      s.click(); await wait(100);
      const dl = Array.from(d.querySelectorAll('button')).find(b=>vis(b)&&(b.innerText||'').trim()==='下载');
      if(!dl) return {{ok:false,step:'noDl'}};
      dl.click();
      return {{ok:true}};
    }});
    if(!ret.ok) throw new Error('submitTopSummary fail: '+JSON.stringify(ret));
    const resp=await waitResp;
    const data=await resp.json().catch(async()=>JSON.parse(await resp.text()));
    const taskId=pickTaskId(data);
    if(!taskId) throw new Error('no taskId from top summary');
    await sleep(1200);
    return taskId;
  }}

  async function submitOneclickAndGetTaskId(mode){{
    const modeText = mode==='daily' ? '分天下载' : '汇总下载';
    const waitResp = page.waitForResponse(r=>r.url().includes('/operate/flow/flowEntrance/asyncExportFlowEntranceDetailList') && r.request().method()==='POST',{{timeout:90000}});
    const ret = await page.evaluate(async (mt)=>{{
      const vis=(el)=>{{
        if(!el) return false;
        const cs = getComputedStyle(el);
        if(cs.display==='none' || cs.visibility==='hidden') return false;
        const r = el.getBoundingClientRect();
        return r.width > 2 && r.height > 2;
      }};
      const wait=(ms)=>new Promise(r=>setTimeout(r,ms));
      const inside = Array.from(document.querySelectorAll('.el-tabs__item')).find(el=>vis(el)&&((el.innerText||'').trim()==='站内流量入口分布'));
      if(inside){{ inside.click(); await wait(450); }}

      const one = Array.from(document.querySelectorAll('button.el-button.export.el-button--text,button,span,div'))
        .filter(b=>vis(b) && (b.innerText||'').includes('一键下载数据'))
        .sort((a,b)=>a.getBoundingClientRect().width - b.getBoundingClientRect().width)[0];
      if(!one) return {{ok:false,step:'noOne'}};
      one.scrollIntoView({{block:'center'}});
      one.click(); await wait(250);

      const d = Array.from(document.querySelectorAll('.el-dialog__wrapper')).find(w=>vis(w)&&(w.innerText||'').includes('下载数据'));
      if(d){{
        const t = Array.from(d.querySelectorAll('span.el-radio-button__inner')).find(s=>(s.innerText||'').trim()===mt);
        if(!t) return {{ok:false,step:'noMode'}};
        t.click(); await wait(120);
        const dl = Array.from(d.querySelectorAll('button')).find(b=>vis(b)&&(b.innerText||'').trim()==='下载');
        if(!dl) return {{ok:false,step:'noDl'}};
        dl.click();
        return {{ok:true,flow:'dialog'}};
      }}

      const modeOpt = Array.from(document.querySelectorAll('li,span,button,div'))
        .find(el => vis(el) && (el.innerText || '').trim() === mt);
      if(modeOpt){{ modeOpt.click(); await wait(120); }}
      const dl2 = Array.from(document.querySelectorAll('button,span,div'))
        .find(el => vis(el) && (el.innerText || '').trim() === '下载');
      if(dl2){{ dl2.click(); await wait(120); }}
      return {{ok:true,flow:'fallback'}};
    }}, modeText);
    if(!ret.ok) throw new Error('submitOneclick fail: '+JSON.stringify(ret));
    const resp=await waitResp;
    const data=await resp.json().catch(async()=>JSON.parse(await resp.text()));
    const taskId=pickTaskId(data);
    if(!taskId) throw new Error('no taskId from oneclick');
    await sleep(1200);
    return taskId;
  }}

  async function downloadByTaskId(taskType, taskId, prefix, kind){{
    await page.goto(DC_BASE + String(taskType), {{waitUntil:'domcontentloaded',timeout:90000}});
    await sleep(1400);

    for(let i=0;i<Number(spec.max_retries||120);i++){{
      const q = page.locator('button:has-text("查询"), span:has-text("查询")').first();
      if(await q.count()) await q.click().catch(()=>{{}});
      await sleep(1800);

      const hit = await page.evaluate((tid)=>{{
        const rows = Array.from(document.querySelectorAll('tbody tr')).map((tr,idx)=>{{
          const tds=tr.querySelectorAll('td');
          const id=((tds[0]?.textContent)||'').replace(/\\s+/g,'').trim();
          const status=(tds[5]?.textContent||'').trim();
          const hasDl=!!tr.querySelector('td:nth-child(7) button');
          const name=((tr.querySelector('.status-name-span')?.getAttribute('title'))||tr.querySelector('.status-name-span')?.textContent||'').trim();
          return {{row:idx+1,id,status,hasDl,name}};
        }});
        return rows.find(r=>r.id===String(tid)) || null;
      }}, taskId);

      if(hit && hit.hasDl && hit.status.includes('成功')){{
        const btn = page.locator(`tbody tr:nth-child(${{hit.row}}) td:nth-child(7) button:has-text("下载")`).first();
        const sanitize=(s)=>String(s||'').replace(/[\\\\/:*?"<>|]/g,'_').replace(/\\s+/g,' ').trim();
        const [dl] = await Promise.all([page.waitForEvent('download',{{timeout:60000}}), btn.click()]);
        const suggestedRaw = sanitize(await dl.suggestedFilename());
        const byTaskName = sanitize(hit.name || '');
        const base = byTaskName || suggestedRaw || String(taskId || 'download');
        const ensured = /\\.(xlsx|xls|csv)$/i.test(base) ? base : `${{base}}.xlsx`;
        const finalName = `${{prefix}}_${{kind}}_${{ensured}}`;
        const path = `${{SAVE}}/${{finalName}}`;
        await dl.saveAs(path);
        return {{taskId, taskType, taskName:hit.name, path}};
      }}
    }}
    throw new Error(`downloadByTaskId timeout taskType=${{taskType}} taskId=${{taskId}}`);
  }}

  async function runOne(ym,prefix,kind){{
    await setMonthAndQuery(ym);
    let taskType=4006;
    let taskId='';
    if(kind==='summary'){{
      taskId = await submitTopSummaryAndGetTaskId();
      taskType = 4006;
    }} else if(kind==='oneclick_summary'){{
      taskId = await submitOneclickAndGetTaskId('summary');
      taskType = 4001;
    }} else {{
      taskId = await submitOneclickAndGetTaskId('daily');
      taskType = 4001;
    }}
    return await downloadByTaskId(taskType, taskId, prefix, kind);
  }}

  async function runOneWithRetry(ym,prefix,kind){{
    const maxTry = Number(spec.per_kind_retries || 3);
    let lastErr = '';
    for(let i=1;i<=maxTry;i++){{
      try {{
        return await runOne(ym,prefix,kind);
      }} catch(e) {{
        lastErr = String((e&&e.message)||e);
        await sleep(1200);
      }}
    }}
    throw new Error(lastErr || `runOne failed kind=${{kind}}`);
  }}

  const done=[]; const errors=[];
  for (const yr of [Number(spec.report_year), Number(spec.compare_year)]) {{
    const ym = `${{yr}}-${{String(Number(spec.report_month)).padStart(2,'0')}}`;
    const prefix = String(yr);
    for(const kind of ['summary','oneclick_summary','oneclick_daily']){{
      try {{ done.push(await runOneWithRetry(ym,prefix,kind)); }}
      catch(e) {{ errors.push({{ym,kind,error:String((e&&e.message)||e)}}); }}
    }}
  }}

  return {{ok:errors.length===0,done,errors,finalUrl:page.url()}};
}}
"""


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Download Compass traffic-source files for report year and compare year.")
    add_config_source_args(p, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    p.add_argument("--session", default=SESSION_NAME)
    p.add_argument("--download-dir", default=DEFAULT_DOWNLOAD_DIR)
    p.add_argument("--report-year", type=int, default=0)
    p.add_argument("--report-month", default="")
    p.add_argument("--compare-year", type=int, default=0)
    p.add_argument("--max-retries", type=int, default=120)
    p.add_argument("--per-kind-retries", type=int, default=3)
    return p.parse_args()


def main() -> None:
    args = parse_args()
    cfg = load_runtime_config(args, default_config=Path(r"D:\projects\skills\vip-report\config.yaml"))
    workdir = Path(cfg["paths"]["workdir"]).resolve()
    workdir.mkdir(parents=True, exist_ok=True)

    download_dir = Path(args.download_dir).resolve()
    download_dir.mkdir(parents=True, exist_ok=True)
    report_year, report_month, compare_year = resolve_period(cfg, args.report_year, args.report_month, args.compare_year)
    ensure_browser_session(args.session, cwd=workdir)

    js_path = write_js(
        workdir,
        f"tmp-compass-traffic-source-download-{report_year}{report_month:02d}-{time.time_ns()}.js",
        build_download_js(
            {
                "traffic_url": TRAFFIC_URL,
                "download_center_base": DOWNLOAD_CENTER_BASE,
                "download_dir": str(download_dir),
                "report_year": report_year,
                "report_month": report_month,
                "compare_year": compare_year,
                "max_retries": int(args.max_retries),
                "per_kind_retries": int(args.per_kind_retries),
            }
        ),
    )
    try:
        out = run_code(args.session, js_path, cwd=workdir, timeout=2400)
    finally:
        try:
            js_path.unlink()
        except FileNotFoundError:
            pass

    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()
