"""Backfill short_sale_daily from FINRA CDN daily files.

FINRA publishes daily short-volume files at:
  https://cdn.finra.org/equity/regsho/daily/CNMSshvol{yyyymmdd}.txt

This script downloads each file for the requested date range and bulk-inserts
into the short_sale_daily table, filtering to the midlarge universe only.

Usage:
  .venv/bin/python3 scripts/backfill_finra_short_volume_cdn.py \
      --start-date 2025-07-01 --end-date 2026-04-22

Idempotent: uses ON CONFLICT DO NOTHING.
"""
from __future__ import annotations

import argparse
import asyncio
import datetime as dt
import os
import sys
import time
from pathlib import Path

import asyncpg
import requests
import yaml

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from libs.common.config import get_settings
from libs.common.time_utils import trading_days_between

FINRA_CDN_URL = "https://cdn.finra.org/equity/regsho/daily/CNMSshvol{yyyymmdd}.txt"
UNIVERSE_FILE = "configs/symbols_midlarge_snapshot_exact.yaml"
BATCH_SIZE = 1000


def load_universe() -> set[str]:
    with open(UNIVERSE_FILE) as f:
        udata = yaml.safe_load(f)
    syms = udata.get("symbols", udata) if isinstance(udata, dict) else udata
    return {s.upper() for s in syms}


def fetch_day(date: dt.date, universe: set[str]) -> list[dict]:
    url = FINRA_CDN_URL.format(yyyymmdd=date.strftime("%Y%m%d"))
    try:
        resp = requests.get(url, timeout=15)
    except requests.RequestException as e:
        print(f"  {date}: request error: {e}")
        return []

    if resp.status_code != 200:
        return []

    lines = [l.strip() for l in resp.text.splitlines() if l.strip()]
    if not lines:
        return []

    header = [p.strip() for p in lines[0].split("|")]
    try:
        sym_idx = header.index("Symbol")
        short_idx = header.index("ShortVolume")
        total_idx = header.index("TotalVolume")
    except ValueError:
        return []

    rows = []
    for line in lines[1:]:
        parts = [p.strip() for p in line.split("|")]
        if len(parts) <= max(sym_idx, short_idx, total_idx):
            continue
        sym = parts[sym_idx].upper()
        if sym not in universe:
            continue
        try:
            short_vol = int(float(parts[short_idx]))
            total_vol = int(float(parts[total_idx]))
        except (ValueError, IndexError):
            continue
        if total_vol <= 0:
            continue
        rows.append({
            "ticker_raw": sym,
            "trade_date": date,
            "short_volume": short_vol,
            "short_exempt_volume": None,
            "total_volume": total_vol,
        })
    return rows


async def insert_batch(conn: asyncpg.Connection, rows: list[dict]) -> int:
    if not rows:
        return 0
    now = dt.datetime.now(tz=dt.timezone.utc)
    inserted = await conn.executemany(
        """
        INSERT INTO short_sale_daily
            (ticker_raw, trade_date, short_volume, short_exempt_volume, total_volume, source_name, created_at_utc)
        VALUES ($1, $2, $3, $4, $5, 'finra_cdn', $6)
        ON CONFLICT ON CONSTRAINT uq_short_sale_ticker_date_source DO NOTHING
        """,
        [
            (r["ticker_raw"], r["trade_date"], r["short_volume"],
             r["short_exempt_volume"], r["total_volume"], now)
            for r in rows
        ],
    )
    return len(rows)


async def main(start_date: dt.date, end_date: dt.date) -> None:
    universe = load_universe()
    print(f"Universe: {len(universe)} tickers")

    # Get trading days in range
    trading_days = [
        d for d in trading_days_between(start_date, end_date)
        if start_date <= d <= end_date
    ]
    print(f"Trading days: {len(trading_days)} ({trading_days[0]} → {trading_days[-1]})")

    dsn = str(get_settings().postgres_dsn).replace("+asyncpg", "")
    conn = await asyncpg.connect(dsn)

    total_rows = 0
    total_days = 0

    try:
        pending: list[dict] = []
        for i, day in enumerate(trading_days):
            rows = fetch_day(day, universe)
            if rows:
                pending.extend(rows)
                total_days += 1
                if (i + 1) % 20 == 0:
                    print(f"  [{i+1}/{len(trading_days)}] day={day}, pending={len(pending)}")
            else:
                if (i + 1) % 20 == 0:
                    print(f"  [{i+1}/{len(trading_days)}] day={day}, skip (no data)")

            if len(pending) >= BATCH_SIZE:
                n = await insert_batch(conn, pending)
                total_rows += n
                pending = []
                print(f"    → inserted batch, total={total_rows}")

            time.sleep(0.05)  # gentle rate limiting

        if pending:
            n = await insert_batch(conn, pending)
            total_rows += n

    finally:
        await conn.close()

    print(f"\nDone. {total_days} trading days fetched, {total_rows} rows inserted.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--start-date", default="2025-07-01")
    parser.add_argument("--end-date", default=dt.date.today().isoformat())
    args = parser.parse_args()

    asyncio.run(main(
        dt.date.fromisoformat(args.start_date),
        dt.date.fromisoformat(args.end_date),
    ))
