"""CSV-based storage backend."""

from __future__ import annotations

import csv
import os
from pathlib import Path
from typing import Any, Dict, List, Optional

from sd_api.exceptions import StorageError
from sd_api.logging import get_logger
from sd_api.models.kline import KlineBar
from sd_api.models.quote import Quote
from sd_api.storage.base import BaseStore

logger = get_logger("storage.csv")


class CSVStore(BaseStore):
    """Simple CSV-based storage.

    Stores quote and kline data as CSV files under a base directory.
    Automatically creates subdirectories per symbol.

    Usage::

        store = CSVStore("./data")
        store.save_quote(quote)
        store.save_kline(bars, symbol="XAUUSD", tf="5min")

        quotes = store.load_quotes("XAUUSD")
    """

    _QUOTE_COLS = [
        "symbol", "bid", "ask", "spread", "mid",
        "received_at", "quote_ts_ms", "quote_time_shanghai",
        "server_ts_ms", "server_time_shanghai", "latency_ms", "is_stale",
    ]
    _BAR_COLS = ["symbol", "tf", "ts", "open", "high", "low", "close"]

    def __init__(self, base_dir: str = "./sd_api_data") -> None:
        self.base_dir = Path(base_dir)
        self.quotes_dir = self.base_dir / "quotes"
        self.kline_dir = self.base_dir / "kline"
        self.quotes_dir.mkdir(parents=True, exist_ok=True)
        self.kline_dir.mkdir(parents=True, exist_ok=True)

    def _quote_path(self, symbol: str) -> Path:
        return self.quotes_dir / f"{symbol.upper().strip()}.csv"

    def _kline_path(self, symbol: str, tf: str) -> Path:
        sym_dir = self.kline_dir / symbol.upper().strip()
        sym_dir.mkdir(exist_ok=True)
        return sym_dir / f"{tf}.csv"

    # ── quote I/O ──────────────────────────────────────────────────────────

    def save_quote(self, quote: Quote) -> None:
        self.save_quotes([quote])

    def save_quotes(self, quotes: List[Quote]) -> None:
        if not quotes:
            return
        by_symbol: Dict[str, List[Quote]] = {}
        for q in quotes:
            by_symbol.setdefault(q.symbol, []).append(q)
        for symbol, qs in by_symbol.items():
            path = self._quote_path(symbol)
            mode = "a" if path.exists() else "w"
            try:
                with open(path, mode, newline="", encoding="utf-8") as f:
                    writer = csv.DictWriter(
                        f,
                        fieldnames=self._QUOTE_COLS,
                        extrasaction="ignore",
                    )
                    if mode == "w":
                        writer.writeheader()
                    for q in qs:
                        row = q.to_dict()
                        # flatten mid
                        row["mid"] = q.mid
                        writer.writerow(row)
            except OSError as e:
                raise StorageError(f"Failed to write CSV {path}: {e}") from e
            logger.debug(f"Saved {len(qs)} quotes to {path}")

    def load_quotes(self, symbol: str, *, limit: int = 1000) -> List[Quote]:
        path = self._quote_path(symbol)
        if not path.exists():
            return []
        quotes: List[Quote] = []
        try:
            with open(path, newline="", encoding="utf-8") as f:
                reader = csv.DictReader(f)
                for i, row in enumerate(reader):
                    if i >= limit:
                        break
                    try:
                        row["bid"] = float(row["bid"])
                        row["ask"] = float(row["ask"])
                        if row.get("spread"):
                            row["spread"] = float(row["spread"])
                        if row.get("quote_ts_ms"):
                            row["quote_ts_ms"] = int(row["quote_ts_ms"])
                        if row.get("server_ts_ms"):
                            row["server_ts_ms"] = int(row["server_ts_ms"])
                        if row.get("latency_ms"):
                            row["latency_ms"] = int(row["latency_ms"])
                        row["is_stale"] = row.get("is_stale") == "True"
                        quotes.append(Quote.from_api(row))
                    except (KeyError, ValueError) as e:
                        logger.warning(f"Skipping bad CSV row: {e}")
        except OSError as e:
            raise StorageError(f"Failed to read CSV {path}: {e}") from e
        return quotes

    # ── kline I/O ─────────────────────────────────────────────────────────

    def save_kline(self, bars: List[KlineBar], symbol: str = "", tf: str = "") -> None:
        if not bars:
            return
        if not symbol or not tf:
            raise StorageError("symbol and tf are required for save_kline")
        path = self._kline_path(symbol, tf)
        mode = "a" if path.exists() else "w"
        try:
            with open(path, mode, newline="", encoding="utf-8") as f:
                writer = csv.DictWriter(
                    f,
                    fieldnames=self._BAR_COLS,
                    extrasaction="ignore",
                )
                if mode == "w":
                    writer.writeheader()
                for bar in bars:
                    writer.writerow({
                        "symbol": symbol.upper(),
                        "tf": tf,
                        "ts": bar.ts,
                        "open": bar.open,
                        "high": bar.high,
                        "low": bar.low,
                        "close": bar.close,
                    })
        except OSError as e:
            raise StorageError(f"Failed to write kline CSV {path}: {e}") from e
        logger.debug(f"Saved {len(bars)} bars to {path}")

    def load_kline(
        self,
        symbol: str,
        *,
        tf: str = "5min",
        limit: int = 500,
    ) -> List[KlineBar]:
        path = self._kline_path(symbol, tf)
        if not path.exists():
            return []
        bars: List[KlineBar] = []
        try:
            with open(path, newline="", encoding="utf-8") as f:
                reader = csv.DictReader(f)
                for i, row in enumerate(reader):
                    if i >= limit:
                        break
                    try:
                        bars.append(KlineBar(
                            ts=int(row["ts"]),
                            open=float(row["open"]),
                            high=float(row["high"]),
                            low=float(row["low"]),
                            close=float(row["close"]),
                        ))
                    except (KeyError, ValueError) as e:
                        logger.warning(f"Skipping bad kline CSV row: {e}")
        except OSError as e:
            raise StorageError(f"Failed to read kline CSV {path}: {e}") from e
        return bars
