from __future__ import annotations

import asyncio
import os
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Any
import glob
import shutil

import pyarrow as pa
import pyarrow.parquet as pq


@dataclass
class _Series:
    bars: List[Dict[str, Any]]
    last_flushed_ts: int


class LiveStore:
    def __init__(self, max_bars: int = 5000, root: str = "data/parquet/live") -> None:
        self._series: Dict[Tuple[str, str], _Series] = {}
        self._max_bars = max_bars
        self._root = root
        self._lock = asyncio.Lock()

    @property
    def root(self) -> str:
        return self._root

    def _get_series(self, symbol: str, timeframe: str) -> _Series:
        key = (symbol, timeframe)
        s = self._series.get(key)
        if not s:
            s = _Series(bars=[], last_flushed_ts=0)
            self._series[key] = s
        return s

    def get_history(self, symbol: str, timeframe: str) -> List[Dict[str, Any]]:
        s = self._get_series(symbol, timeframe)
        return list(s.bars)

    def ingest_bar(self, symbol: str, timeframe: str, bar: Dict[str, Any]) -> None:
        s = self._get_series(symbol, timeframe)
        if s.bars and s.bars[-1]["time"] == bar["time"]:
            # update last
            last = s.bars[-1]
            last["high"] = max(last["high"], bar["high"])
            last["low"] = min(last["low"], bar["low"])
            last["close"] = bar["close"]
            last["volume"] = last.get("volume", 0.0) + bar.get("volume", 0.0)
        else:
            s.bars.append(bar)
            if len(s.bars) > self._max_bars:
                s.bars.pop(0)

    async def flush_parquet(self) -> None:
        # Write new bars since last flush, partitioned by date
        async with self._lock:
            for (symbol, timeframe), s in self._series.items():
                new_rows = [b for b in s.bars if b["time"] > s.last_flushed_ts]
                if not new_rows:
                    continue
                # Partition by date
                rows_by_date: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
                for r in new_rows:
                    dt = datetime.utcfromtimestamp(int(r["time"]))
                    rows_by_date[dt.strftime("%Y-%m-%d")].append(r)
                for date_str, rows in rows_by_date.items():
                    table = pa.Table.from_pylist(rows)
                    base = os.path.join(self._root, symbol, timeframe)
                    out = os.path.join(base, f"date={date_str}")
                    os.makedirs(out, exist_ok=True)
                    # write one file per flush to this partition
                    pq.write_table(table, os.path.join(out, f"part-{int(datetime.utcnow().timestamp())}.parquet"))
                s.last_flushed_ts = max(b["time"] for b in new_rows)


# Singleton store
live_store = LiveStore()


async def periodic_flush(interval_sec: int = 60):
    while True:
        try:
            await live_store.flush_parquet()
        except Exception:
            pass
        await asyncio.sleep(interval_sec)


def _iter_partitions(root: str):
    """Yield (symbol, timeframe, partition_path, date_str) for existing partitions."""
    # root/symbol/timeframe/date=YYYY-MM-DD
    for sym_dir in glob.glob(f"{root}/*"):
        if not os.path.isdir(sym_dir):
            continue
        symbol = os.path.basename(sym_dir)
        for tf_dir in glob.glob(f"{sym_dir}/*"):
            if not os.path.isdir(tf_dir):
                continue
            timeframe = os.path.basename(tf_dir)
            for part_dir in glob.glob(f"{tf_dir}/date=*" ):
                if not os.path.isdir(part_dir):
                    continue
                date_str = os.path.basename(part_dir).split("=", 1)[-1]
                yield (symbol, timeframe, part_dir, date_str)


def prune_old_partitions(root: str, retention_days: int = 7) -> int:
    """Delete partition directories older than retention_days. Returns count deleted."""
    now = datetime.utcnow()
    deleted = 0
    for symbol, timeframe, part_dir, date_str in list(_iter_partitions(root)):
        try:
            y, m, d = map(int, date_str.split("-"))
            dt = datetime(y, m, d)
            if now - dt > timedelta(days=retention_days):
                shutil.rmtree(part_dir, ignore_errors=True)
                deleted += 1
        except Exception:
            # Skip unparsable date partitions
            continue
    return deleted


def compact_partition(part_dir: str, max_files_threshold: int = 20) -> bool:
    """If too many small part files exist, compact them into a single file.
    Returns True if compaction performed.
    """
    part_files = sorted(glob.glob(os.path.join(part_dir, "part-*.parquet")))
    if len(part_files) < max_files_threshold:
        return False
    try:
        tables: List[pa.Table] = []
        for p in part_files:
            tables.append(pq.read_table(p))
        if not tables:
            return False
        combined = pa.concat_tables(tables, promote=True)
        out_file = os.path.join(part_dir, f"compact-{int(datetime.utcnow().timestamp())}.parquet")
        pq.write_table(combined, out_file)
        # remove old parts
        for p in part_files:
            try:
                os.remove(p)
            except Exception:
                pass
        return True
    except Exception:
        return False


def compact_all(root: str, max_files_threshold: int = 20) -> int:
    """Run compaction across all partitions. Returns number of partitions compacted."""
    compacted = 0
    for _, _, part_dir, _ in list(_iter_partitions(root)):
        if compact_partition(part_dir, max_files_threshold=max_files_threshold):
            compacted += 1
    return compacted


async def periodic_maintenance(retention_days: int = 7, compact_threshold_files: int = 20, interval_sec: int = 900):
    """Periodically prune old partitions and compact small files."""
    while True:
        try:
            prune_old_partitions(live_store.root, retention_days=retention_days)
            compact_all(live_store.root, max_files_threshold=compact_threshold_files)
        except Exception:
            pass
        await asyncio.sleep(interval_sec)
