"""Cian history backfill через real Chrome (Playwright) — обходит anti-bot detection.

Iterates Cian listings без offer_price_history, открывает в headless Chrome,
extract'ит defaultState.offerData.priceChanges через page.evaluate, пишет в БД.

Requires:
  pip install playwright "sqlalchemy>=2" "psycopg[binary]"
  playwright install chromium

Run:
  $env:TRADEIN_DB_DSN = "postgresql+psycopg://tradein:PASS@localhost:15433/tradein"
  python C:\\Users\\user\\cian-backfill\\playwright_history.py --batch 20 --delay 2

Flags:
  --headed     показать браузер (debug)
  --batch N    листингов за батч (default 20)
  --delay S    seconds между listings (default 2)
  --max N      hard cap (default 5500)
"""
from __future__ import annotations

import argparse
import asyncio
import os
import signal
import sys
import time
from datetime import datetime, timezone

import json

import psycopg
from playwright.async_api import async_playwright

EXTRACT_JS = """
() => {
  if (!window._cianConfig || !window._cianConfig['frontend-offer-card']) {
    return { error: 'no _cianConfig', captcha: location.href.includes('captcha') };
  }
  const arr = window._cianConfig['frontend-offer-card'];
  const def = arr.find(e => e && e.key === 'defaultState');
  if (!def || !def.value) {
    return { error: 'no defaultState' };
  }
  const v = def.value;
  const offerData = v.offerData || {};
  const offer = offerData.offer || {};
  // priceChanges живёт в offerData.priceChanges (НЕ в offerData.offer.priceChanges)
  const priceChanges = offerData.priceChanges || offer.priceChanges || v.priceChanges || [];
  // bargainTerms.price — текущая цена
  const price_rub = (offer.bargainTerms && offer.bargainTerms.price) || null;
  // Также собрать ключи offerData чтобы видеть что доступно
  return {
    cian_id: offer.id || null,
    price_rub,
    price_changes: priceChanges,
    _offerData_keys: Object.keys(offerData).slice(0, 25),
    _offer_keys: Object.keys(offer).slice(0, 25),
  };
}
"""


def parse_change_time(s):
    """Parse 'YYYY-MM-DDTHH:MM:SS' or ISO → timestamptz; return raw string if parse fails."""
    if not s:
        return None
    return s  # psycopg позволяет передать строку → postgres парсит как timestamptz


def upsert_changes(cur, listing_id: int, price_rub: int | None, changes: list) -> int:
    """Insert priceChanges array; return count inserted (ignoring conflicts)."""
    if not changes:
        return 0
    rows = []
    prev_price = None
    for c in changes:
        ct = c.get("changeTime") or c.get("date")
        # priceData.price — основной формат 2026+; fallback на старые поля
        pd = c.get("priceData") if isinstance(c.get("priceData"), dict) else None
        pr = (pd.get("price") if pd else None) or c.get("price") or c.get("priceRub")
        if not ct or not pr:
            continue
        diff_pct = None
        if prev_price is not None and prev_price > 0:
            diff_pct = round((pr - prev_price) / prev_price * 100, 2)
        rows.append((listing_id, ct, pr, diff_pct, "cian"))
        prev_price = pr
    if not rows:
        return 0
    cur.executemany(
        """
        INSERT INTO offer_price_history (listing_id, change_time, price_rub, diff_percent, source)
        VALUES (%s, %s, %s, %s, %s)
        ON CONFLICT (listing_id, change_time) DO NOTHING
        """,
        rows,
    )
    return cur.rowcount or 0


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--batch", type=int, default=20)
    ap.add_argument("--delay", type=float, default=2.0)
    ap.add_argument("--max", type=int, default=5500)
    ap.add_argument("--headed", action="store_true")
    args = ap.parse_args()

    dsn = os.environ.get("TRADEIN_DB_DSN")
    if not dsn:
        sys.exit("FATAL: TRADEIN_DB_DSN env required")
    # psycopg native dsn (без +psycopg для sqlalchemy)
    pg_dsn = dsn.replace("postgresql+psycopg://", "postgresql://")

    interrupted = False

    def on_sigint(s, f):
        nonlocal interrupted
        print("\n!! Ctrl+C — finishing current item")
        interrupted = True
    signal.signal(signal.SIGINT, on_sigint)

    async def run() -> None:
        nonlocal interrupted
        cum_ok = cum_fail = cum_changes = cum_with_history = 0
        start = time.time()
        last_id = 0  # paginate forward — избегаем loop когда changes=0

        # Optional: load cookies из БД для авторизованной сессии
        cookies_from_db: list = []
        try:
            with psycopg.connect(pg_dsn) as conn:
                with conn.cursor() as cur:
                    enc_key = os.environ.get("COOKIE_ENCRYPTION_KEY")
                    if enc_key:
                        cur.execute(
                            """
                            SELECT pgp_sym_decrypt(cookies_encrypted, %s)::text
                            FROM cian_session_cookies
                            WHERE expires_at_estimate > NOW() AND last_invalid_at IS NULL
                            ORDER BY uploaded_at DESC LIMIT 1
                            """,
                            (enc_key,),
                        )
                        r = cur.fetchone()
                        if r:
                            jar = json.loads(r[0])
                            for name, val in jar.items():
                                cookies_from_db.append({
                                    "name": name, "value": val,
                                    "domain": ".cian.ru", "path": "/",
                                    "secure": True, "sameSite": "Lax",
                                })
            print(f"[init] loaded {len(cookies_from_db)} cookies from DB")
        except Exception as exc:
            print(f"[init] cookies load skipped: {exc}")

        async with async_playwright() as p:
            browser = await p.chromium.launch(headless=not args.headed)
            ctx = await browser.new_context(
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
                viewport={"width": 1366, "height": 900},
                locale="ru-RU",
            )
            if cookies_from_db:
                await ctx.add_cookies(cookies_from_db)
            page = await ctx.new_page()
            page.set_default_navigation_timeout(45_000)
            page.set_default_timeout(45_000)

            processed = 0
            while not interrupted and processed < args.max:
                # 1. Fetch batch — paginate id > last_id чтобы не зацикливаться
                with psycopg.connect(pg_dsn) as conn:
                    with conn.cursor() as cur:
                        cur.execute(
                            """
                            SELECT l.id, l.source_url
                            FROM listings l
                            LEFT JOIN offer_price_history oph ON oph.listing_id = l.id
                            WHERE l.source = 'cian'
                              AND l.source_url IS NOT NULL
                              AND oph.listing_id IS NULL
                              AND l.id > %s
                            ORDER BY l.id
                            LIMIT %s
                            """,
                            (last_id, args.batch),
                        )
                        rows = cur.fetchall()
                if not rows:
                    print(f"\n[done] drained — no more listings")
                    break
                print(f"\n[batch] {len(rows)} listings to process (cum: ok={cum_ok} fail={cum_fail} changes={cum_changes})")

                # 2. Process each
                with psycopg.connect(pg_dsn, autocommit=False) as conn:
                    for (lid, url) in rows:
                        if interrupted:
                            break
                        processed += 1
                        t0 = time.time()
                        try:
                            await page.goto(url, wait_until="domcontentloaded")
                            # wait for _cianConfig populated
                            await page.wait_for_function(
                                "() => window._cianConfig && window._cianConfig['frontend-offer-card'] && window._cianConfig['frontend-offer-card'].some(e => e.key === 'defaultState')",
                                timeout=15_000,
                            )
                            data = await page.evaluate(EXTRACT_JS)
                            if data.get("error"):
                                if data.get("captcha"):
                                    print(f"  [{lid}] CAPTCHA — please solve in browser (use --headed)")
                                    interrupted = True
                                    break
                                cum_fail += 1
                                print(f"  [{lid}] {data['error']} {time.time() - t0:.1f}s")
                                continue
                            changes = data.get("price_changes") or []
                            with conn.cursor() as cur:
                                inserted = upsert_changes(cur, lid, data.get("price_rub"), changes)
                                conn.commit()
                            cum_ok += 1
                            cum_changes += inserted
                            if len(changes) > 0:
                                cum_with_history += 1
                            print(f"  [{lid}] OK cian_id={data.get('cian_id')} price={data.get('price_rub')} changes={len(changes)} inserted={inserted} {time.time() - t0:.1f}s")
                            # На первом обращении и затем каждые 50 — dump для diagnostics
                            if processed == 1 or processed % 50 == 0:
                                print(f"    [diag] offerData_keys={data.get('_offerData_keys')}")
                                print(f"    [diag] offer_keys={data.get('_offer_keys')}")
                            # Dump shape priceChanges первые 3 раза + при первом успехе
                            if (changes and processed <= 3) or (changes and inserted > 0 and cum_changes == inserted):
                                print(f"    [diag] price_changes_sample={changes[0]}")
                            last_id = lid  # advance pagination cursor even on 0-change result
                        except Exception as exc:
                            conn.rollback()
                            cum_fail += 1
                            print(f"  [{lid}] ERR {type(exc).__name__}: {str(exc)[:120]} {time.time() - t0:.1f}s")
                        await asyncio.sleep(args.delay)

            await browser.close()

        print()
        print(f"== Summary ==")
        print(f"   processed_ok={cum_ok} fail={cum_fail}")
        print(f"   listings_with_history={cum_with_history}  price_changes_inserted={cum_changes}")
        print(f"   elapsed={time.time() - start:.1f}s  finished={datetime.now(timezone.utc).isoformat(timespec='seconds')}")

    asyncio.run(run())
    return 0


if __name__ == "__main__":
    sys.exit(main())
