__all__ = ['connect', 'execute']

import logging
import re
import pymssql as mssql
import pandas as pd

_log = logging.getLogger(__name__)

# Allow only safe stored-procedure names: letters, digits, underscores, dots.
# This guards against injection if callers construct proc_name from user input.
_PROC_NAME_RE = re.compile(r'^[\w\.]+$')


def connect(conn_params):
    return mssql.connect(*conn_params)

def execute(proc_name, conn_params, proc_params, commit=True):
    """
    Executes a stored-procedure and returns the first result set as a DataFrame.
    :param proc_name: the fully qualified stored procedure name
    :param conn_params: a tuple containing the connection parameters (server, user, password, database)
    :param proc_params: a tuple containing the procedure parameters
    :param commit: whether to commit the transaction after execution. Default True.
    :return: a DataFrame of the first returned dataset, or None if no rows returned
    :raises ValueError: if ``proc_name`` contains characters outside ``[\\w.]``.
    :raises pymssql.Error: re-raises any database error after logging it.
    """
    if not _PROC_NAME_RE.match(proc_name):
        raise ValueError(
            "proc_name '{0}' contains invalid characters. "
            "Only letters, digits, underscores, and dots are allowed.".format(
                proc_name))

    result = None

    try:
        with connect(conn_params) as conn:
            with conn.cursor(as_dict=True) as cursor:
                cursor.callproc(
                    proc_name,
                    proc_params)

                if cursor.nextset():
                    rows = cursor.fetchall()
                    result = pd.DataFrame.from_records(rows) if rows else pd.DataFrame()

                if commit:
                    conn.commit()
    except mssql.Error:
        _log.error(
            'Database error executing stored procedure "%s".',
            proc_name,
            exc_info=True)
        raise

    return result
