"""Functions to interact with SQLite database"""

import sqlite3
import os
import re
import logging

logger = logging.getLogger(__name__)


def connect_db(db_name, db_create, setstring="", rowfactory=None):
    """Connect to target database

    Parameters
    ----------
    db_name : str
        Name or full file path to database
    db_create : int
        Whether to create database or not, 1 creates
    setstring : str, optional
        Any commands that should be run at connection (e.g. a pragma). Defaults to "".
    rowfactory : str, optional
        Pass "Row" to return dicts instead of tuples. Defaults to None.

    Returns
    -------
    tuple[int, sqlite3.Connection | None, sqlite3.Cursor | None]
        (0, connection, cursor) on success, (error, None, None) on failure
    """
    if rowfactory == "Row":
        rowfactory = sqlite3.Row

    try:
        if not os.path.isfile(db_name) and db_create != 1:
            return -1, None, None

        connection = sqlite3.connect(db_name)
        connection.row_factory = rowfactory
        cursor = connection.cursor()

        if setstring:
            cursor.execute(setstring)

        return 0, connection, cursor

    except sqlite3.Error as error:
        logger.error("Error while connecting to sqlite: %s", error)
        return error, None, None


def run_sql(dbcursor, cmd_text, values=None, return_id=False):
    """Run any SQLite command

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the sqlite database
    cmd_text : str
        The preformatted SQL command
    values : tuple, optional
        Bound parameter values for the query. Defaults to None.
    return_id : bool, optional
        If True, returns the last inserted row id. Use for INSERT statements.

    Returns
    -------
    int | bool | None
        Last inserted row id if return_id=True, True on success, None on failure
    """
    conn = dbcursor.connection

    try:
        with conn:
            if values:
                dbcursor.execute(cmd_text, values)
            else:
                dbcursor.execute(cmd_text)

        if return_id:
            return dbcursor.lastrowid

        return True

    except sqlite3.Error as error:
        logger.error("SQLite error: %s", error)
        return None


def select_existing(dbcursor, cmd_text, values=None):
    """Get information from existing sqlite DB

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the database
    cmd_text : str
        SELECT statement to execute
    values : tuple, optional
        Bound parameter values. Defaults to None.

    Returns
    -------
    list | None
        List of rows returned, or None on error
    """
    try:
        if values:
            dbcursor.execute(cmd_text, values)
        else:
            dbcursor.execute(cmd_text)
        return dbcursor.fetchall()

    except sqlite3.Error as error:
        logger.error("Error executing select: %s", error)
        return None


def check_table_exists(dbcursor, table_name: str):
    """Check if a table exists in the sqlite database

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the sqlite database
    table_name : str
        Name of the table to look for

    Returns
    -------
    bool
        True if table exists, False otherwise
    """
    cmd = "SELECT name FROM sqlite_master WHERE type='table' AND name=?;"
    result = select_existing(dbcursor, cmd, (table_name,))
    return bool(result)


def create_dev_table(dbcursor):
    """Create the devlist table for the inator logging db

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the database

    Returns
    -------
    bool | None
        True on success, None on failure
    """
    cmd = """
        CREATE TABLE IF NOT EXISTS devlist (
            id          INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            name        TEXT NOT NULL,
            guid        INTEGER,
            num_sensors INTEGER,
            info        TEXT,
            type        TEXT,
            location    TEXT,
            active      INTEGER
        );
    """
    return run_sql(dbcursor, cmd)


def insert_dev_table(dbcursor, dev_info: dict):
    """Insert device information into the devlist table

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor for the database
    dev_info : dict
        Device information with keys:
        device_name, device_guid, num_sensors, device_info,
        device_type, device_location, device_active

    Returns
    -------
    int | None
        Inserted row id on success, None on failure
    """
    cmd = """
        INSERT INTO devlist (name, guid, num_sensors, info, type, location, active)
        VALUES (?, ?, ?, ?, ?, ?, ?);
    """
    values = (
        dev_info["device_name"],
        dev_info["device_guid"],
        dev_info["num_sensors"],
        dev_info["device_info"],
        dev_info["device_type"],
        dev_info["device_location"],
        dev_info["device_active"],
    )
    return run_sql(dbcursor, cmd, values, return_id=True)


def _safe_table_name(dev_id: int, dev_name: str, suffix: str = "") -> str:
    """Build a sanitised table name from device id and name

    Parameters
    ----------
    dev_id : int
        Device id from devlist
    dev_name : str
        Device name
    suffix : str, optional
        Optional suffix e.g. '_details'. Defaults to "".

    Returns
    -------
    str
        Sanitised table name
    """
    safe_name = re.sub(r"[^a-zA-Z0-9_]", "_", dev_name)
    return f"{dev_id}_{safe_name}{suffix}"


def create_dev_info_table(dbcursor, dev_id: int, dev_info: dict):
    """Create the device info table for a connected device

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the database
    dev_id : int
        Device id from devlist
    dev_info : dict
        Dictionary of device information

    Returns
    -------
    bool | None
        True on success, None on failure
    """
    table = _safe_table_name(dev_id, dev_info["device_name"], "_details")
    cmd = f"""
        CREATE TABLE IF NOT EXISTS "{table}" (
            sens_id   INTEGER PRIMARY KEY AUTOINCREMENT,
            sens_name TEXT,
            measures  TEXT,
            returns   TEXT,
            calib     TEXT,
            range     TEXT,
            info      TEXT,
            comments  TEXT
        );
    """
    return run_sql(dbcursor, cmd)


def insert_dev_info_table(dbcursor, dev_id: int, dev_info: dict, sens_info: dict):
    """Insert sensor data for a device into its info table

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor for the database
    dev_id : int
        Device id from devlist
    dev_info : dict
        Dictionary of device info
    sens_info : dict
        Dictionary of sensor info

    Returns
    -------
    int | None
        Inserted row id on success, None on failure
    """
    table = _safe_table_name(dev_id, dev_info["device_name"], "_details")
    cmd = f"""
        INSERT INTO "{table}" (sens_name, measures, returns, calib, range, info, comments)
        VALUES (?, ?, ?, ?, ?, ?, ?);
    """
    values = (
        sens_info["sens_name"],
        sens_info["measures"],
        sens_info["returns"],
        sens_info["calib"],
        sens_info["range"],
        sens_info["info"],
        sens_info["comments"],
    )
    return run_sql(dbcursor, cmd, values, return_id=True)


def create_dev_log_table(dbcursor, dev_id: int, dev_info: dict):
    """Create the table to log sensor data for a device

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor for the database
    dev_id : int
        Device id from devlist
    dev_info : dict
        Dictionary of device information

    Returns
    -------
    bool | None
        True on success, None on failure
    """
    table = _safe_table_name(dev_id, dev_info["device_name"])
    sensor_cols = ", ".join(
        [f"id_{n+1} INTEGER" for n in range(dev_info["num_sensors"])]
    )
    cmd = f"""
        CREATE TABLE IF NOT EXISTS "{table}" (
            id        INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
            timestamp INTEGER
            {', ' + sensor_cols if sensor_cols else ''}
        );
    """
    return run_sql(dbcursor, cmd)


def insert_dev_log_table(dbcursor, dev_info: dict, data: dict, key_dict: dict):
    """Insert data into the device log table

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor for the database
    dev_info : dict
        Dictionary of device information
    data : dict
        Data to insert
    key_dict : dict
        Mapping of column id (e.g. id_1) to data key

    Returns
    -------
    int | None
        Inserted row id on success, None on failure
    """
    table = _safe_table_name(dev_info["id"], dev_info["device_name"])
    columns = ["timestamp"] + [f"id_{n+1}" for n in range(dev_info["num_sensors"])]
    placeholders = ", ".join(["?"] * len(columns))
    col_str = ", ".join(columns)

    values = [data["timestamp"]]
    for n in range(dev_info["num_sensors"]):
        nkey = key_dict[f"id_{n+1}"]
        values.append(data[nkey])

    cmd = f'INSERT INTO "{table}" ({col_str}) VALUES ({placeholders});'
    return run_sql(dbcursor, cmd, values, return_id=True)


def get_device_info(dbcursor, inator: str):
    """Get device information from devlist table

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the database
    inator : str
        Name of the device

    Returns
    -------
    tuple[bool, dict | None]
        (True, device_info_dict) if found, (False, None) if not found or error
    """
    cmd = "SELECT id, num_sensors FROM devlist WHERE name=?;"
    result = select_existing(dbcursor, cmd, (inator,))

    if result is None:
        logger.error("SQL error fetching device info for '%s'", inator)
        return False, None

    if not result:
        return False, None

    if len(result) > 1:
        logger.warning(
            "Multiple devices named '%s' found, using first. Consider using GUID.",
            inator,
        )

    dev_inf = {
        "id": result[0][0],
        "device_name": inator,
        "num_sensors": result[0][1],
    }
    return True, dev_inf


def get_data_id_key(dbcursor, dev_info: dict, data: dict):
    """Get the mapping linking incoming data keys to sensor id columns

    Parameters
    ----------
    dbcursor : sqlite3.Cursor
        Cursor to the database
    dev_info : dict
        Dictionary of device info
    data : dict
        Incoming data that needs linking

    Returns
    -------
    dict
        Mapping of column id (e.g. 'id_1') to data key
    """
    table = _safe_table_name(dev_info["id"], dev_info["device_name"], "_details")
    cmd = f'SELECT sens_id FROM "{table}" WHERE sens_name=?;'

    sort_key = {}
    for key in data:
        if key == "timestamp":
            continue
        result = select_existing(dbcursor, cmd, (key,))
        if not result:
            logger.warning("Unknown sensor key '%s' — skipping", key)
        else:
            sort_key[f"id_{result[0][0]}"] = key

    return sort_key
