"""
data_store.py

Persistence for this project: ranked candidate lists, open/closed
position state, trade records, and a full per-cycle indicator dump for
every candidate monitor.py evaluates (not just the ones that trade) --
per explicit instruction to log every indicator through a symbol's
whole lifecycle, same reasoning as sip_bot's append_fast_engine_
decision(): the point is being able to load a day's file into pandas
afterward and see exactly what the engine saw, whether or not it ever
became a real trade.

Same atomic-write pattern (write to .tmp, then os.replace) and the same
fcntl-based singleton lock as screener/premarket's data_store.py.
"""

import fcntl
import json
import os
from datetime import date, datetime
from pathlib import Path
from contextlib import contextmanager

from config_loader import get_config
from logger_setup import get_logger

log = get_logger("data_store")

BASE_DIR = Path(__file__).resolve().parent


def _dirs():
    cfg = get_config()["data_storage"]
    return {
        "candidates": BASE_DIR / cfg["candidates_dir"],
        "trades": BASE_DIR / cfg["trades_dir"],
        "decisions": BASE_DIR / cfg["decisions_dir"],
        "state": BASE_DIR / cfg["state_dir"],
    }


def ensure_dirs():
    for d in _dirs().values():
        d.mkdir(parents=True, exist_ok=True)


def _today_str():
    return date.today().isoformat()


def _atomic_write_json(path: Path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    with open(tmp, "w") as f:
        json.dump(data, f, indent=2, default=str)
    os.replace(tmp, path)


def _read_json(path: Path, default):
    if not path.exists():
        return default
    try:
        with open(path, "r") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError) as e:
        log.warning(f"Failed to read {path}: {e}. Returning default.")
        return default


def _append_jsonl(path: Path, record: dict):
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "a") as f:
        f.write(json.dumps(record, default=str) + "\n")


@contextmanager
def _file_lock(lock_path: Path):
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    with open(lock_path, "w") as lf:
        fcntl.flock(lf, fcntl.LOCK_EX)
        try:
            yield
        finally:
            fcntl.flock(lf, fcntl.LOCK_UN)


def write_candidates(candidates: list):
    """This project's own ranked candidate list -- data/candidates/
    {date}_scanner.json. Date-stamped so a day's runs can be reviewed
    and compared later."""
    path = _dirs()["candidates"] / f"{_today_str()}_scanner.json"
    _atomic_write_json(path, candidates)
    log.info(f"[DATA] Wrote {len(candidates)} candidates to {path}")


# ---------------------------------------------------------------------------
# Positions (mutable state -- current truth) and trades (append-only history)
# ---------------------------------------------------------------------------

def _state_path(name: str) -> Path:
    return _dirs()["state"] / name


def load_positions() -> dict:
    return _read_json(_state_path("positions.json"), {})


def save_positions(positions: dict):
    with _file_lock(_state_path("positions.json.lock")):
        _atomic_write_json(_state_path("positions.json"), positions)


def append_trade_record(trade: dict):
    """One line per closed round trip -- data/trades/{date}_trades.jsonl."""
    path = _dirs()["trades"] / f"{_today_str()}_trades.jsonl"
    _append_jsonl(path, trade)


def write_trades_summary(trades: list):
    path = _dirs()["trades"] / f"{_today_str()}_summary.json"
    _atomic_write_json(path, trades)


def load_today_trades() -> list:
    path = _dirs()["trades"] / f"{_today_str()}_trades.jsonl"
    if not path.exists():
        return []
    trades = []
    with open(path, "r") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                trades.append(json.loads(line))
            except json.JSONDecodeError:
                log.warning(f"Skipping malformed trade record line in {path}")
    return trades


# ---------------------------------------------------------------------------
# Per-cycle decision/indicator log -- every candidate, every poll
# ---------------------------------------------------------------------------

def append_decision_record(record: dict):
    """One JSON line per symbol per poll cycle -- data/decisions/
    {date}.jsonl. Carries the FULL smart_engine metrics dict (every
    indicator: vwap, ema9/20, atr, trend, rvol, spread, structure
    flags, stage3 target/stop/reward-risk, everything), not just the
    classified state, so a day's file can be loaded into pandas and
    show exactly what the engine saw for every symbol at every poll --
    whether or not it ever became a real trade."""
    path = _dirs()["decisions"] / f"{_today_str()}.jsonl"
    record = {"timestamp": datetime.utcnow().isoformat(), **record}
    _append_jsonl(path, record)


# ---------------------------------------------------------------------------
# Singleton lock -- prevents two monitor.py instances against one account
# ---------------------------------------------------------------------------

def acquire_singleton_lock() -> "SingletonLock":
    return SingletonLock(_state_path("monitor.pid.lock"))


class SingletonLock:
    def __init__(self, path: Path):
        self.path = path
        self._fh = None

    def __enter__(self):
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._fh = open(self.path, "w")
        try:
            fcntl.flock(self._fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            raise RuntimeError(
                "Another monitor.py instance already holds the singleton lock. "
                "Refusing to start a second instance against the same account."
            )
        self._fh.write(str(os.getpid()))
        self._fh.flush()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self._fh:
            fcntl.flock(self._fh, fcntl.LOCK_UN)
            self._fh.close()
