"""Constantes, acces HTTP et schema SQLite partages par collector.py et analyze.py (stdlib uniquement)."""
import json
import os
import sqlite3
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone

GAMMA = "https://gamma-api.polymarket.com"
CLOB = "https://clob.polymarket.com"
DATA = "https://data-api.polymarket.com"
BINANCE = "https://api.binance.com"

ASSETS = ("btc", "eth", "sol", "xrp")
SYMBOLS = {"btc": "BTCUSDT", "eth": "ETHUSDT", "sol": "SOLUSDT", "xrp": "XRPUSDT"}

TF_SECONDS = {"5m": 300, "15m": 900, "1h": 3600, "4h": 14400, "1d": 86400}
# Intervalle d'echantillonnage du carnet par horizon de marche (s) : plus la fenetre est courte, plus on echantillonne vite.
TF_SAMPLE = {"5m": 5, "15m": 10, "1h": 30, "4h": 60, "1d": 120}

_HOURLY = {"btc": "btc-up-or-down-hourly", "eth": "eth-up-or-down-hourly",
           "sol": "solana-up-or-down-hourly", "xrp": "xrp-up-or-down-hourly"}
_DAILY = {"btc": "btc-up-or-down-daily", "eth": "eth-up-or-down-daily",
          "sol": "solana-up-or-down-daily", "xrp": "xrp-up-or-down-daily"}


def series_slug(asset, tf):
    if tf == "1h":
        return _HOURLY[asset]
    if tf == "1d":
        return _DAILY[asset]
    return f"{asset}-up-or-down-{tf}"


def default_db_path():
    # Hors du dossier synchronise (Google Drive) : une base SQLite ecrite en continu s'y corromprait.
    base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
    return os.path.join(base, "market-prediction", "pm.db")


def parse_duration(s):
    s = s.strip().lower()
    unit = {"s": 1, "m": 60, "h": 3600, "d": 86400}[s[-1]]
    return int(float(s[:-1]) * unit)


def iso_ts(s):
    return int(datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp())


def ts_iso(t):
    return datetime.fromtimestamp(t, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def http_json(url, params=None, body=None, timeout=15, tries=4):
    if params:
        url += ("&" if "?" in url else "?") + urllib.parse.urlencode(params)
    headers = {"User-Agent": "market-prediction-research/0.1"}
    data = None
    if body is not None:
        data = json.dumps(body).encode()
        headers["Content-Type"] = "application/json"
    err = None
    for i in range(tries):
        try:
            req = urllib.request.Request(url, data=data, headers=headers)
            with urllib.request.urlopen(req, timeout=timeout) as r:
                return json.loads(r.read())
        except urllib.error.HTTPError as e:
            err = e
            if e.code != 429 and e.code < 500:
                raise
        except (urllib.error.URLError, TimeoutError, ConnectionError, json.JSONDecodeError, OSError) as e:
            err = e
        time.sleep(min(2 ** i, 10))
    raise RuntimeError(f"echec HTTP {url[:120]}: {err}")


SCHEMA = """
CREATE TABLE IF NOT EXISTS markets(
  id INTEGER PRIMARY KEY,       -- identifiant entier reutilise dans book / flow (base 2 a 3 fois plus compacte)
  condition_id TEXT UNIQUE,
  slug TEXT, asset TEXT, tf TEXT,
  start_ts INTEGER, end_ts INTEGER,
  up_token TEXT, down_token TEXT,
  resolution_source TEXT,
  price_to_beat REAL, final_price REAL,
  outcome INTEGER,              -- 1 = Up, 0 = Down, -1 = inconnu, NULL = en attente
  volume REAL,
  flow_done INTEGER DEFAULT 0,  -- 1 = flux de transactions agrege, 2 = tronque
  hist_done INTEGER DEFAULT 0,  -- 1 = historique de cote 1 min recupere
  first_seen INTEGER
);
-- Etat du carnet du jeton Up (le carnet Down en est le miroir exact : bid_down = 1 - ask_up).
CREATE TABLE IF NOT EXISTS book(
  ts REAL, mkt INTEGER, src TEXT,         -- mkt = markets.id ; src : 'live' (collecteur) ou 'hist' (prices-history 1 min)
  bid REAL, ask REAL, mid REAL, last REAL,
  bid_sz1 REAL, ask_sz1 REAL,             -- taille au meilleur prix
  bid_d5 REAL, ask_d5 REAL,               -- profondeur a moins de 5 cts du mid (parts)
  bid_d10 REAL, ask_d10 REAL,
  n_bids INTEGER, n_asks INTEGER,
  book_ts INTEGER                          -- horodatage serveur CLOB (ms)
);
CREATE INDEX IF NOT EXISTS book_mkt_ts ON book(mkt, ts);
CREATE TABLE IF NOT EXISTS spot(ts REAL, symbol TEXT, bid REAL, ask REAL, mid REAL);
CREATE INDEX IF NOT EXISTS spot_sym_ts ON spot(symbol, ts);
-- Transactions Polymarket agregees par tranches de 10 s. up_pressure = $ nets "pour Up"
-- (achat Up + vente Down - vente Up - achat Down).
CREATE TABLE IF NOT EXISTS flow(
  mkt INTEGER, bucket_ts INTEGER,
  up_pressure REAL, gross REAL, n_trades INTEGER, n_wallets INTEGER, vwap_up REAL,
  PRIMARY KEY(mkt, bucket_ts)
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS klines(
  symbol TEXT, open_ts INTEGER, o REAL, h REAL, l REAL, c REAL, v REAL,
  PRIMARY KEY(symbol, open_ts)
);
-- Indicateurs externes (external.py). ts = instant ou la valeur devient DISPONIBLE (jamais avant).
CREATE TABLE IF NOT EXISTS ext(
  source TEXT, symbol TEXT, key TEXT, ts INTEGER, value REAL,
  PRIMARY KEY(source, symbol, key, ts)
) WITHOUT ROWID;
-- Messages Stocktwits : etiquette choisie par l'auteur (1 = Bullish, -1 = Bearish, 0 = aucune).
CREATE TABLE IF NOT EXISTS stwits(id INTEGER PRIMARY KEY, symbol TEXT, ts INTEGER, sent INTEGER);
CREATE INDEX IF NOT EXISTS stwits_sym_ts ON stwits(symbol, ts);
-- Signaux du moteur en direct (paper trading) : une mise fictive par signal.
CREATE TABLE IF NOT EXISTS signals(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  strategy_id INTEGER, asset TEXT, ts REAL, side INTEGER, score REAL,
  entry REAL, target REAL, stop REAL, expiry REAL, unit_usd REAL,
  status TEXT,                  -- 'open' | 'closed'
  exit_ts REAL, exit_price REAL, exit_reason TEXT, pnl_usd REAL,
  info TEXT                     -- JSON : probabilites, esperance, facteurs principaux au moment du signal
);
"""


def connect(path):
    os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
    con = sqlite3.connect(path, timeout=60, check_same_thread=False)
    # Plusieurs programmes peuvent ouvrir la base au meme instant (lancer.bat) : passer en WAL exige un acces
    # exclusif et SQLite ne patiente pas toujours ; on reessaie au lieu de s'arreter.
    for attempt in range(60):
        try:
            if con.execute("PRAGMA journal_mode").fetchone()[0].lower() != "wal":
                con.execute("PRAGMA journal_mode=WAL")  # reglage memorise dans le fichier : une seule fois
            con.execute("PRAGMA synchronous=NORMAL")
            con.executescript(SCHEMA)
            # bases creees avant l'ajout de la colonne
            if "info" not in [r[1] for r in con.execute("PRAGMA table_info(signals)")]:
                con.execute("ALTER TABLE signals ADD COLUMN info TEXT")
                con.commit()
            return con
        except sqlite3.OperationalError as e:
            if "locked" not in str(e) or attempt == 59:
                raise
            time.sleep(2)


def single_instance(name, db):
    """Empeche de lancer deux fois le meme programme sur la meme base (double-clic repete sur lancer.bat).
    Le verrou est libere automatiquement a l'arret, meme en cas de plantage. Garder la valeur retournee."""
    f = open(os.path.join(os.path.dirname(os.path.abspath(db)), f"{name}.lock"), "a+")
    try:
        if os.name == "nt":
            import msvcrt
            f.seek(0)
            msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
        else:
            import fcntl
            fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except OSError:
        sys.exit(f"{name} tourne deja sur cette base ({db}) : cette fenetre peut etre fermee.")
    return f


def parse_market(m, asset, tf):
    """Extrait une ligne `markets` d'un marche Gamma (dict). Retourne None si inexploitable."""
    try:
        toks = json.loads(m["clobTokenIds"])
        outs = [o.lower() for o in json.loads(m["outcomes"])]
    except (KeyError, TypeError, ValueError):
        return None
    if len(toks) != 2 or "up" not in outs:
        return None
    up_i = outs.index("up")
    end = iso_ts(m["endDate"])
    start = iso_ts(m["eventStartTime"]) if m.get("eventStartTime") else end - TF_SECONDS[tf]
    meta = ((m.get("events") or [{}])[0].get("eventMetadata")) or m.get("_eventMetadata") or {}
    outcome = None
    if m.get("closed"):
        try:
            prices = [float(p) for p in json.loads(m["outcomePrices"])]
            if max(prices) >= 0.99:
                outcome = 1 if prices[up_i] >= 0.99 else 0
        except (KeyError, TypeError, ValueError):
            pass
    return {
        "condition_id": m["conditionId"], "slug": m.get("slug"), "asset": asset, "tf": tf,
        "start_ts": start, "end_ts": end,
        "up_token": toks[up_i], "down_token": toks[1 - up_i],
        "resolution_source": m.get("resolutionSource"),
        "price_to_beat": meta.get("priceToBeat"), "final_price": meta.get("finalPrice"),
        "outcome": outcome, "volume": float(m.get("volumeNum") or m.get("volume") or 0),
    }


def upsert_market(con, r):
    con.execute(
        """INSERT INTO markets(condition_id, slug, asset, tf, start_ts, end_ts, up_token, down_token,
               resolution_source, price_to_beat, final_price, outcome, volume, first_seen)
           VALUES(:condition_id, :slug, :asset, :tf, :start_ts, :end_ts, :up_token, :down_token,
               :resolution_source, :price_to_beat, :final_price, :outcome, :volume, strftime('%s','now'))
           ON CONFLICT(condition_id) DO UPDATE SET
               price_to_beat = COALESCE(excluded.price_to_beat, price_to_beat),
               final_price = COALESCE(excluded.final_price, final_price),
               outcome = COALESCE(excluded.outcome, outcome),
               volume = MAX(COALESCE(excluded.volume, 0), COALESCE(volume, 0))""", r)
    return con.execute("SELECT id FROM markets WHERE condition_id = ?", (r["condition_id"],)).fetchone()[0]
