import sqlite3
from typing import Any, Iterable, List, Tuple, Optional, Dict


class SQLiteDB:
    """Thin wrapper around sqlite3 that exposes CRUD helpers."""

    def __init__(self, db_path: str = ":memory:") -> None:
        """
        Open a connection to the SQLite database.

        Parameters
        ----------
        db_path : str
            Path to the database file. Use ':memory:' for an in‑memory DB.
        """
        self.db_path = db_path
        self.conn: sqlite3.Connection = sqlite3.connect(self.db_path)
        # Row factory makes rows behave like dictionaries
        self.conn.row_factory = sqlite3.Row
        self.cursor: sqlite3.Cursor = self.conn.cursor()

    # ------------------------------------------------------------------
    # Table helpers
    # ------------------------------------------------------------------
    def create_users_table(self) -> None:
        """
        Create a simple 'users' table if it does not exist.

        Table schema:
            id   INTEGER PRIMARY KEY AUTOINCREMENT
            name TEXT NOT NULL
            age  INTEGER
        """
        sql = """
        CREATE TABLE IF NOT EXISTS users (
            id   INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            age  INTEGER
        );
        """
        self.cursor.execute(sql)
        self.conn.commit()

    # ------------------------------------------------------------------
    # CRUD
    # ------------------------------------------------------------------
    def insert_user(self, name: str, age: Optional[int] = None) -> int:
        """
        Insert a new user.

        Returns
        -------
        int
            The newly created row id.
        """
        sql = "INSERT INTO users (name, age) VALUES (?, ?)"
        self.cursor.execute(sql, (name, age))
        self.conn.commit()
        return self.cursor.lastrowid

    def get_user(self, user_id: int) -> Optional[Dict[str, Any]]:
        """
        Retrieve a single user by id.

        Returns
        -------
        dict or None
            Row as a dictionary if found, otherwise None.
        """
        sql = "SELECT * FROM users WHERE id = ?"
        self.cursor.execute(sql, (user_id,))
        row = self.cursor.fetchone()
        return dict(row) if row else None

    def get_all_users(self) -> List[Dict[str, Any]]:
        """
        Retrieve all users.

        Returns
        -------
        list of dict
            All rows as dictionaries.
        """
        sql = "SELECT * FROM users"
        self.cursor.execute(sql)
        rows = self.cursor.fetchall()
        return [dict(row) for row in rows]

    def update_user(self, user_id: int, name: Optional[str] = None,
                    age: Optional[int] = None) -> bool:
        """
        Update a user's name and/or age.

        Parameters
        ----------
        user_id : int
            The id of the user to update.
        name : str | None
            New name (or None to leave unchanged).
        age : int | None
            New age (or None to leave unchanged).

        Returns
        -------
        bool
            True if a row was updated, False otherwise.
        """
        # Build dynamic SET clause
        fields: List[str] = []
        values: List[Any] = []

        if name is not None:
            fields.append("name = ?")
            values.append(name)
        if age is not None:
            fields.append("age = ?")
            values.append(age)

        if not fields:
            # Nothing to update
            return False

        values.append(user_id)
        sql = f"UPDATE users SET {', '.join(fields)} WHERE id = ?"
        self.cursor.execute(sql, tuple(values))
        self.conn.commit()
        return self.cursor.rowcount > 0

    def delete_user(self, user_id: int) -> bool:
        """
        Delete a user by id.

        Returns
        -------
        bool
            True if a row was deleted, False otherwise.
        """
        sql = "DELETE FROM users WHERE id = ?"
        self.cursor.execute(sql, (user_id,))
        self.conn.commit()
        return self.cursor.rowcount > 0

    # ------------------------------------------------------------------
    # Utility
    # ------------------------------------------------------------------
    def close(self) -> None:
        """Close the database connection."""
        self.cursor.close()
        self.conn.close()

    # Context manager support
    def __enter__(self) -> "SQLiteDB":
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
        self.close()
