"""
SC2 Replay Database Layer

SQLite-based storage for parsed replay data.
"""
import sqlite3
from contextlib import contextmanager
from typing import Optional

from .config import get_db_path, ensure_config_dir


SCHEMA = """
CREATE TABLE IF NOT EXISTS replays (
    replay_id TEXT PRIMARY KEY,
    file_path TEXT,
    played_at TEXT,
    map_name TEXT,
    player_race TEXT,
    opponent_race TEXT,
    matchup TEXT,
    result TEXT,
    game_length_sec INTEGER,
    player_mmr INTEGER,
    opponent_mmr INTEGER,
    player_apm INTEGER,
    opponent_apm INTEGER,
    opponent_name TEXT,

    -- Worker metrics
    workers_6m INTEGER,
    workers_8m INTEGER,
    workers_10m INTEGER,

    -- Base metrics
    bases_by_6m INTEGER,
    bases_by_8m INTEGER,
    bases_by_10m INTEGER,
    natural_timing INTEGER,
    third_timing INTEGER,

    -- Army metrics
    army_supply_8m INTEGER,
    army_minerals_8m INTEGER,
    army_gas_8m INTEGER,
    worker_kills_8m INTEGER,
    worker_losses_8m INTEGER,
    first_attack_time INTEGER,

    -- Game classification
    game_category TEXT,
    is_ladder INTEGER,
    is_ranked INTEGER,
    region TEXT,

    -- Metadata
    parsed_at TEXT
);

CREATE INDEX IF NOT EXISTS idx_played_at ON replays(played_at DESC);
CREATE INDEX IF NOT EXISTS idx_matchup ON replays(matchup);
CREATE INDEX IF NOT EXISTS idx_result ON replays(result);
CREATE INDEX IF NOT EXISTS idx_map_name ON replays(map_name);
"""

TAGS_SCHEMA = """
CREATE TABLE IF NOT EXISTS tags (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tag_date TEXT NOT NULL,
    label TEXT NOT NULL,
    end_date TEXT,
    created_at TEXT NOT NULL,
    UNIQUE(tag_date, label)
);
CREATE INDEX IF NOT EXISTS idx_tags_date ON tags(tag_date DESC);
CREATE INDEX IF NOT EXISTS idx_tags_label ON tags(label);
"""

REPLAY_NOTES_SCHEMA = """
CREATE TABLE IF NOT EXISTS replay_notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    replay_id TEXT NOT NULL UNIQUE,
    analysis_json TEXT NOT NULL,
    created_at TEXT NOT NULL,
    updated_at TEXT,
    FOREIGN KEY (replay_id) REFERENCES replays(replay_id)
);
CREATE INDEX IF NOT EXISTS idx_replay_notes_replay ON replay_notes(replay_id);
"""


def init_db():
    """Initialize the database and create tables if needed."""
    ensure_config_dir()
    with get_connection() as conn:
        conn.executescript(SCHEMA)
        conn.executescript(TAGS_SCHEMA)
        conn.executescript(REPLAY_NOTES_SCHEMA)
        # Migrations: add columns that may be missing from older databases
        _migrate_add_column(conn, "bases_by_6m", "INTEGER")
        _migrate_add_column(conn, "bases_by_8m", "INTEGER")
        _migrate_add_column(conn, "bases_by_10m", "INTEGER")
        _migrate_add_column(conn, "opponent_name", "TEXT")
        # Game classification migrations
        _migrate_add_column(conn, "game_category", "TEXT")
        _migrate_add_column(conn, "is_ladder", "INTEGER")
        _migrate_add_column(conn, "is_ranked", "INTEGER")
        _migrate_add_column(conn, "region", "TEXT")
        # Tags table migrations
        _migrate_add_column(conn, "end_date", "TEXT", table="tags")


def _migrate_add_column(conn, column_name: str, column_type: str, table: str = "replays"):
    """Add a column to a table if it doesn't exist."""
    cursor = conn.execute(f"PRAGMA table_info({table})")
    existing_columns = {row[1] for row in cursor.fetchall()}
    if column_name not in existing_columns:
        conn.execute(f"ALTER TABLE {table} ADD COLUMN {column_name} {column_type}")


@contextmanager
def get_connection():
    """Context manager for database connections."""
    conn = sqlite3.connect(str(get_db_path()))
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    finally:
        conn.close()


def replay_exists(replay_id: str) -> bool:
    """Check if a replay is already in the database."""
    with get_connection() as conn:
        cursor = conn.execute(
            "SELECT 1 FROM replays WHERE replay_id = ?",
            (replay_id,)
        )
        return cursor.fetchone() is not None


def insert_replay(data: dict):
    """Insert a parsed replay into the database."""
    columns = list(data.keys())
    placeholders = ", ".join("?" * len(columns))
    column_names = ", ".join(columns)

    with get_connection() as conn:
        conn.execute(
            f"INSERT OR REPLACE INTO replays ({column_names}) VALUES ({placeholders})",
            tuple(data.values())
        )


def get_replays(
    matchup: Optional[str] = None,
    result: Optional[str] = None,
    map_name: Optional[str] = None,
    days: Optional[int] = None,
    limit: Optional[int] = None,
    min_length: Optional[int] = None,
    max_length: Optional[int] = None,
    min_workers_8m: Optional[int] = None,
    max_workers_8m: Optional[int] = None,
    player_race: Optional[str] = None,
    is_ladder: Optional[bool] = None,
    region: Optional[str] = None,
) -> list:
    """
    Query replays with optional filters.

    Returns list of dictionaries, ordered by played_at descending.
    """
    query = "SELECT * FROM replays WHERE 1=1"
    params = []

    if matchup:
        query += " AND UPPER(matchup) = UPPER(?)"
        params.append(matchup)

    if result:
        query += " AND LOWER(result) = LOWER(?)"
        params.append(result)

    if map_name:
        query += " AND map_name LIKE ?"
        params.append(f"%{map_name}%")

    if days:
        query += " AND played_at >= datetime('now', ?)"
        params.append(f"-{days} days")

    if min_length is not None:
        query += " AND game_length_sec >= ?"
        params.append(min_length)

    if max_length is not None:
        query += " AND game_length_sec <= ?"
        params.append(max_length)

    if min_workers_8m is not None:
        query += " AND workers_8m >= ?"
        params.append(min_workers_8m)

    if max_workers_8m is not None:
        query += " AND workers_8m <= ?"
        params.append(max_workers_8m)

    if player_race:
        query += " AND LOWER(player_race) = LOWER(?)"
        params.append(player_race)

    if is_ladder is not None:
        if is_ladder:
            # Include NULL for pre-migration data compatibility
            # Also exclude detected unranked games (is_ranked=0)
            query += " AND (is_ladder = 1 OR is_ladder IS NULL)"
            query += " AND (is_ranked = 1 OR is_ranked IS NULL)"
        else:
            query += " AND is_ladder = 0"

    if region:
        query += " AND LOWER(region) = LOWER(?)"
        params.append(region)

    query += " ORDER BY played_at DESC"

    if limit:
        query += " LIMIT ?"
        params.append(limit)

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        return [dict(row) for row in cursor.fetchall()]


def get_latest_replay() -> Optional[dict]:
    """Get the most recently played replay."""
    replays = get_replays(limit=1)
    return replays[0] if replays else None


def get_stats(
    matchup: Optional[str] = None,
    days: Optional[int] = None,
    player_race: Optional[str] = None,
    is_ladder: Optional[bool] = None,
    region: Optional[str] = None,
) -> dict:
    """
    Get aggregate statistics.

    Returns dict with total games, wins, losses, averages.
    """
    query = """
        SELECT
            COUNT(*) as total_games,
            SUM(CASE WHEN LOWER(result) = 'win' THEN 1 ELSE 0 END) as wins,
            SUM(CASE WHEN LOWER(result) = 'loss' THEN 1 ELSE 0 END) as losses,
            AVG(workers_6m) as avg_workers_6m,
            AVG(workers_8m) as avg_workers_8m,
            AVG(workers_10m) as avg_workers_10m,
            AVG(army_supply_8m) as avg_army_supply_8m,
            AVG(game_length_sec) as avg_game_length
        FROM replays
        WHERE 1=1
    """
    params = []

    if matchup:
        query += " AND UPPER(matchup) = UPPER(?)"
        params.append(matchup)

    if days:
        query += " AND played_at >= datetime('now', ?)"
        params.append(f"-{days} days")

    if player_race:
        query += " AND LOWER(player_race) = LOWER(?)"
        params.append(player_race)

    if is_ladder is not None:
        if is_ladder:
            query += " AND (is_ladder = 1 OR is_ladder IS NULL)"
        else:
            query += " AND is_ladder = 0"

    if region:
        query += " AND LOWER(region) = LOWER(?)"
        params.append(region)

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        row = cursor.fetchone()
        return dict(row) if row else {}


def get_stats_by_matchup(
    days: Optional[int] = None,
    player_race: Optional[str] = None,
    is_ladder: Optional[bool] = None,
    region: Optional[str] = None,
) -> list:
    """Get stats grouped by matchup."""
    query = """
        SELECT
            matchup,
            COUNT(*) as total_games,
            SUM(CASE WHEN LOWER(result) = 'win' THEN 1 ELSE 0 END) as wins,
            AVG(workers_8m) as avg_workers_8m
        FROM replays
        WHERE 1=1
    """
    params = []

    if days:
        query += " AND played_at >= datetime('now', ?)"
        params.append(f"-{days} days")

    if player_race:
        query += " AND LOWER(player_race) = LOWER(?)"
        params.append(player_race)

    if is_ladder is not None:
        if is_ladder:
            query += " AND (is_ladder = 1 OR is_ladder IS NULL)"
        else:
            query += " AND is_ladder = 0"

    if region:
        query += " AND LOWER(region) = LOWER(?)"
        params.append(region)

    query += " GROUP BY matchup ORDER BY total_games DESC"

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        return [dict(row) for row in cursor.fetchall()]


def get_replay_count() -> int:
    """Get total number of replays in database."""
    with get_connection() as conn:
        cursor = conn.execute("SELECT COUNT(*) FROM replays")
        return cursor.fetchone()[0]


def get_streaks(
    streak_type: str,
    min_length: int = 3,
    matchup: Optional[str] = None,
    map_name: Optional[str] = None,
    days: Optional[int] = None,
) -> list:
    """
    Find streaks of consecutive wins or losses.

    Args:
        streak_type: "win" or "loss" - type of streak to find
        min_length: Minimum streak length to include (default 3)
        matchup: Optional matchup filter (e.g., "TvZ")
        map_name: Optional map name filter (partial match)
        days: Optional filter for last N days

    Returns:
        List of replay dicts for all games in qualifying streaks,
        INCLUDING the game that ends each streak.
    """
    # Build query to get all replays ordered chronologically (oldest first)
    query = "SELECT * FROM replays WHERE 1=1"
    params = []

    if matchup:
        query += " AND UPPER(matchup) = UPPER(?)"
        params.append(matchup)

    if map_name:
        query += " AND map_name LIKE ?"
        params.append(f"%{map_name}%")

    if days:
        query += " AND played_at >= datetime('now', ?)"
        params.append(f"-{days} days")

    query += " ORDER BY played_at ASC"

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        all_replays = [dict(row) for row in cursor.fetchall()]

    if not all_replays:
        return []

    # Determine the target result based on streak type
    target_result = "Win" if streak_type.lower() == "win" else "Loss"

    result_replays = []
    current_streak = []

    for replay in all_replays:
        replay_result = (replay.get("result") or "").capitalize()

        if replay_result == target_result:
            # Continue streak
            current_streak.append(replay)
        else:
            # Streak ended - check if it qualifies
            if len(current_streak) >= min_length:
                # Add all games from the streak plus the ending game
                result_replays.extend(current_streak)
                result_replays.append(replay)
            # Start fresh
            current_streak = []

    # Note: We don't include ongoing streaks at the end (no ending game)
    # since user requested streaks "ending with a loss/win"

    # Return in descending order (newest first) for display
    return list(reversed(result_replays))


def expand_results(
    replays: list,
    prev_count: int = 0,
    next_count: int = 0,
) -> list:
    """
    Expand replay results with adjacent games.

    Args:
        replays: Current list of replays (ordered by played_at DESC)
        prev_count: Number of games before the oldest result to add
        next_count: Number of games after the newest result to add

    Returns:
        Expanded list with additional games added
    """
    if not replays:
        return replays

    # Get replay IDs to exclude duplicates
    existing_ids = {r["replay_id"] for r in replays}

    # Get timestamps for boundary queries
    # replays[0] is newest (highest timestamp), replays[-1] is oldest (lowest timestamp)
    newest_timestamp = replays[0]["played_at"]
    oldest_timestamp = replays[-1]["played_at"]

    result = list(replays)  # Copy current results

    # Query for next games (newer than newest)
    if next_count > 0:
        query = "SELECT * FROM replays WHERE played_at > ? ORDER BY played_at ASC LIMIT ?"
        with get_connection() as conn:
            cursor = conn.execute(query, (newest_timestamp, next_count))
            next_games = [dict(row) for row in cursor.fetchall()]
        # Filter out duplicates and prepend (reversed to maintain DESC order)
        next_games = [g for g in next_games if g["replay_id"] not in existing_ids]
        result = list(reversed(next_games)) + result

    # Query for previous games (older than oldest)
    if prev_count > 0:
        query = "SELECT * FROM replays WHERE played_at < ? ORDER BY played_at DESC LIMIT ?"
        with get_connection() as conn:
            cursor = conn.execute(query, (oldest_timestamp, prev_count))
            prev_games = [dict(row) for row in cursor.fetchall()]
        # Filter out duplicates and append (already in DESC order)
        prev_games = [g for g in prev_games if g["replay_id"] not in existing_ids]
        result = result + prev_games

    return result


def get_unique_map_names() -> list:
    """Get list of unique map names from database."""
    query = "SELECT DISTINCT map_name FROM replays WHERE map_name IS NOT NULL ORDER BY map_name"
    with get_connection() as conn:
        cursor = conn.execute(query)
        return [row[0] for row in cursor.fetchall()]


# ============================================================
# TAGS
# ============================================================


def add_tag(tag_date: str, label: str, end_date: Optional[str] = None) -> bool:
    """Add a tag to a date.

    Args:
        tag_date: Start date in YYYY-MM-DD format
        label: Tag label text
        end_date: Optional end date for completed ranges (YYYY-MM-DD)

    Returns:
        True if tag was added, False if duplicate
    """
    from datetime import datetime

    with get_connection() as conn:
        try:
            conn.execute(
                "INSERT INTO tags (tag_date, label, end_date, created_at) VALUES (?, ?, ?, ?)",
                (tag_date, label, end_date, datetime.now().isoformat()),
            )
            return True
        except sqlite3.IntegrityError:
            # Duplicate tag (same date + label)
            return False


def get_ongoing_tags() -> list:
    """Get all ongoing tags (tags without an end_date).

    Returns:
        List of dicts with id, tag_date, label, end_date, created_at
    """
    query = "SELECT * FROM tags WHERE end_date IS NULL ORDER BY tag_date DESC"
    with get_connection() as conn:
        cursor = conn.execute(query)
        return [dict(row) for row in cursor.fetchall()]


def end_tag(label: str, end_date: str) -> bool:
    """Set end_date for an ongoing tag.

    Args:
        label: Tag label to close
        end_date: End date in YYYY-MM-DD format

    Returns:
        True if tag was ended, False if no matching ongoing tag found
    """
    with get_connection() as conn:
        cursor = conn.execute(
            "UPDATE tags SET end_date = ? WHERE label = ? AND end_date IS NULL",
            (end_date, label),
        )
        return cursor.rowcount > 0


def get_tags(tag_date: Optional[str] = None) -> list:
    """Get tags, optionally filtered by date.

    Args:
        tag_date: Optional date filter (YYYY-MM-DD)

    Returns:
        List of dicts with id, tag_date, label, created_at
    """
    if tag_date:
        query = "SELECT * FROM tags WHERE tag_date = ? ORDER BY created_at"
        params = (tag_date,)
    else:
        query = "SELECT * FROM tags ORDER BY tag_date DESC, created_at"
        params = ()

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        return [dict(row) for row in cursor.fetchall()]


def get_tagged_dates() -> set:
    """Get set of dates that have tags.

    Returns:
        Set of date strings (YYYY-MM-DD) for O(1) lookup
    """
    query = "SELECT DISTINCT tag_date FROM tags"
    with get_connection() as conn:
        cursor = conn.execute(query)
        return {row[0] for row in cursor.fetchall()}


def remove_tag(tag_date: str, label: Optional[str] = None) -> int:
    """Remove tag(s) for a date.

    Args:
        tag_date: Date in YYYY-MM-DD format
        label: If provided, only remove this specific tag.
               If None, remove ALL tags for the date.

    Returns:
        Number of tags removed
    """
    with get_connection() as conn:
        if label:
            cursor = conn.execute(
                "DELETE FROM tags WHERE tag_date = ? AND label = ?",
                (tag_date, label),
            )
        else:
            cursor = conn.execute(
                "DELETE FROM tags WHERE tag_date = ?",
                (tag_date,),
            )
        return cursor.rowcount


# ============================================================
# RACE DETECTION
# ============================================================


def get_most_played_race() -> Optional[str]:
    """Get the race with the most games played.

    Returns:
        Race name (e.g., "Terran") or None if no replays
    """
    query = """
        SELECT player_race, COUNT(*) as cnt
        FROM replays
        WHERE player_race IS NOT NULL
        GROUP BY player_race
        ORDER BY cnt DESC
        LIMIT 1
    """
    with get_connection() as conn:
        cursor = conn.execute(query)
        row = cursor.fetchone()
        return row[0] if row else None


def get_race_distribution() -> list:
    """Get game count per race.

    Returns:
        List of dicts with race and count, ordered by count descending
    """
    query = """
        SELECT player_race as race, COUNT(*) as count
        FROM replays
        WHERE player_race IS NOT NULL
        GROUP BY player_race
        ORDER BY count DESC
    """
    with get_connection() as conn:
        cursor = conn.execute(query)
        return [dict(row) for row in cursor.fetchall()]


# ============================================================
# REGION DETECTION
# ============================================================


def get_most_played_region() -> Optional[str]:
    """Get the region with the most games played.

    Returns:
        Region string (e.g., "eu") or None if no replays
    """
    query = """
        SELECT region, COUNT(*) as cnt
        FROM replays
        WHERE region IS NOT NULL
        GROUP BY region
        ORDER BY cnt DESC
        LIMIT 1
    """
    with get_connection() as conn:
        cursor = conn.execute(query)
        row = cursor.fetchone()
        return row[0] if row else None


def get_region_distribution() -> list:
    """Get game count per region.

    Returns:
        List of dicts with region and count, ordered by count descending
    """
    query = """
        SELECT region, COUNT(*) as count
        FROM replays
        WHERE region IS NOT NULL
        GROUP BY region
        ORDER BY count DESC
    """
    with get_connection() as conn:
        cursor = conn.execute(query)
        return [dict(row) for row in cursor.fetchall()]


# ============================================================
# RANKED/UNRANKED DETECTION
# ============================================================

MMR_JUMP_THRESHOLD = 150


def detect_unranked_games():
    """Detect unranked games by MMR discontinuity.

    SC2 replays don't distinguish ranked from unranked. We detect unranked
    games by finding same-race games where MMR jumps by more than the
    threshold between consecutive games (ranked MMR changes ~20 per game).

    Updates is_ranked column: 1=ranked, 0=likely unranked.
    Returns the number of games flagged as unranked.
    """
    with get_connection() as conn:
        # Get all ladder games ordered by race then time
        cursor = conn.execute("""
            SELECT replay_id, player_race, player_mmr, played_at
            FROM replays
            WHERE (is_ladder = 1 OR is_ladder IS NULL)
                AND player_mmr IS NOT NULL
                AND player_race IS NOT NULL
            ORDER BY player_race, played_at ASC
        """)
        rows = [dict(r) for r in cursor.fetchall()]

    if not rows:
        return 0

    # Group by race and detect jumps
    unranked_ids = set()
    prev_by_race = {}  # race -> last MMR

    for row in rows:
        race = row["player_race"]
        mmr = row["player_mmr"]
        replay_id = row["replay_id"]

        if race in prev_by_race:
            prev_mmr = prev_by_race[race]
            if abs(mmr - prev_mmr) > MMR_JUMP_THRESHOLD:
                # This game has a big MMR jump - likely switched ranked/unranked
                unranked_ids.add(replay_id)

        prev_by_race[race] = mmr

    # Now we have games at jump points. We need to figure out which side is
    # unranked. Strategy: for each race, find the "main" MMR cluster (the one
    # with more games) and flag the outlier cluster as unranked.
    # Group consecutive games by MMR proximity
    race_games = {}
    for row in rows:
        race = row["player_race"]
        if race not in race_games:
            race_games[race] = []
        race_games[race].append(row)

    final_unranked_ids = set()
    for race, games in race_games.items():
        if len(games) < 3:
            continue

        # Find MMR clusters using simple median split
        mmrs = [g["player_mmr"] for g in games]
        median_mmr = sorted(mmrs)[len(mmrs) // 2]

        # Games far below median are likely unranked
        for game in games:
            if abs(game["player_mmr"] - median_mmr) > MMR_JUMP_THRESHOLD:
                final_unranked_ids.add(game["replay_id"])

    # Update database
    flagged = 0
    with get_connection() as conn:
        for row in rows:
            is_ranked = 0 if row["replay_id"] in final_unranked_ids else 1
            conn.execute(
                "UPDATE replays SET is_ranked = ? WHERE replay_id = ?",
                (is_ranked, row["replay_id"]),
            )
            if is_ranked == 0:
                flagged += 1

    return flagged


# ============================================================
# REPLAY NOTES
# ============================================================


def save_replay_notes(replay_id: str, analysis_json: str) -> bool:
    """Save or update analysis notes for a replay.

    Args:
        replay_id: Replay ID to attach notes to
        analysis_json: JSON string with analysis data

    Returns:
        True if saved successfully
    """
    from datetime import datetime

    now = datetime.now().isoformat()
    with get_connection() as conn:
        conn.execute(
            """INSERT INTO replay_notes (replay_id, analysis_json, created_at, updated_at)
               VALUES (?, ?, ?, ?)
               ON CONFLICT(replay_id) DO UPDATE SET
                   analysis_json = excluded.analysis_json,
                   updated_at = excluded.updated_at""",
            (replay_id, analysis_json, now, now),
        )
        return True


def get_replay_notes(replay_id: str) -> Optional[dict]:
    """Get analysis notes for a specific replay.

    Returns:
        Dict with id, replay_id, analysis_json, created_at, updated_at or None
    """
    with get_connection() as conn:
        cursor = conn.execute(
            "SELECT * FROM replay_notes WHERE replay_id = ?",
            (replay_id,),
        )
        row = cursor.fetchone()
        return dict(row) if row else None


def get_all_notes(days: Optional[int] = None, limit: Optional[int] = None) -> list:
    """Get all replay notes with replay context.

    Args:
        days: Optional filter for notes on replays from last N days
        limit: Maximum number of notes to return

    Returns:
        List of dicts with note fields + replay context (played_at, map_name, matchup, result, player_mmr)
    """
    query = """
        SELECT n.*, r.played_at, r.map_name, r.matchup, r.result,
               r.player_mmr, r.opponent_name
        FROM replay_notes n
        JOIN replays r ON n.replay_id = r.replay_id
        WHERE 1=1
    """
    params = []

    if days:
        query += " AND r.played_at >= datetime('now', ?)"
        params.append(f"-{days} days")

    query += " ORDER BY r.played_at DESC"

    if limit:
        query += " LIMIT ?"
        params.append(limit)

    with get_connection() as conn:
        cursor = conn.execute(query, params)
        return [dict(row) for row in cursor.fetchall()]


def delete_replay_notes(replay_id: str) -> bool:
    """Delete analysis notes for a replay.

    Returns:
        True if notes were deleted, False if no notes found
    """
    with get_connection() as conn:
        cursor = conn.execute(
            "DELETE FROM replay_notes WHERE replay_id = ?",
            (replay_id,),
        )
        return cursor.rowcount > 0
