import json
import re

from typing import Optional, Dict, Any
from .__about__ import __version__

from .dbc.Exceptions import SQLConnectionException, SQLQueryException
from .dbc.JDBC import jdbcLoaded, jdbcDriver, jdbcEngine
from .dbc.ODBC import odbcLoaded, odbcDriver, odbcEngine

JDBC_QUERY_KEY = "jdbc-query"
ODBC_QUERY_KEY = "odbc-query"
ERROR_KEY = "error"


class sql:
    def __init__(self):
        self.__cnxn = None
        self.__isConnected = False
        self.__driverOverride = None
        self.__jdbcDriver = None
        self.__jdbcLoaded = None
        self.__odbcDriver = None
        self.__odbcLoaded = None
        self.__connStr = None
        self.__connStatus = None
        self.__connStats = None

    def connect(self,
                server: Optional[str] = None,
                port: Optional[int] = None,
                user: Optional[str] = None,
                password: Optional[str] = None,
                trust: Optional[str] = None,
                db: Optional[str] = "",
                trustServerCertificate: Optional[bool] = True,
                mars: Optional[str] = 'no',
                driverOverride: Optional[str] = None,
                ):

        self.__odbcLoaded = odbcLoaded()
        self.__odbcDriver = odbcDriver()
        self.__jdbcLoaded = jdbcLoaded()
        self.__jdbcDriver = jdbcDriver()
        self.__driverOverride = driverOverride.lower() if driverOverride.lower() in ["odbc", "jdbc"] else "odbc"

        self.__connStr = {
            "server": server,
            "port": port,
            "user": user,
            "password": password,
            "trust": trust,
            "db": db,
            "trustServerCertificate": trustServerCertificate,
            "mars": mars,
            "driverOverride": driverOverride
        }

        if self.__driverOverride == "odbc" and self.__odbcLoaded:
            self.__driverOverride = "odbc"
        elif self.__driverOverride == "jdbc" and self.__jdbcLoaded:
            self.__driverOverride = "jdbc"
        else:  # pragma: no cover
            self.__driverOverride = "jdbc"

        match self.__driverOverride:
            case "odbc":
                self.__cnxn = odbcEngine(
                    server=self.__connStr['server'],
                    port=self.__connStr['port'],
                    db=self.__connStr['db'],
                    user=self.__connStr['user'],
                    password=self.__connStr['password'],
                    trust=self.__connStr['trust'],
                    mars=self.__connStr['mars'],
                )
                self._logging()

                try:
                    self.__cnxn.connect()
                    self.__isConnected = self.__cnxn.isConnected
                    self.__connStats = self.__cnxn.connStats
                except Exception as e:  # pragma: no cover
                    raise SQLConnectionException(f'{e}')

            case _:
                self.__cnxn = jdbcEngine(
                    server=self.__connStr['server'],
                    port=self.__connStr['port'],
                    user=self.__connStr['user'],
                    password=self.__connStr['password'],
                    trustServerCertificate=self.__connStr['trustServerCertificate']
                )
                self._logging()

                try:
                    self.__cnxn.connect()
                    self.__isConnected = self.__cnxn.isConnected
                    self.__connStats = self.__cnxn.connStats
                except Exception as e:  # pragma: no cover
                    raise SQLConnectionException(f'{e}')

    def execute_procedure(self,
                          procedure_name: str,
                          parameters: Optional[Dict[str, Any]] = None,
                          stats: bool = False) -> Optional[Dict[str, Any]]:
        """
        Executes a stored procedure with optional parameters.

        Args:
            procedure_name: Name of the stored procedure to execute
            parameters: Optional dictionary of parameter names and values
            stats: Whether to return execution statistics

        Returns:
            If stats=True, returns dict with 'results' and 'stats' keys
            If stats=False, returns just the results

        Raises:
            SQLQueryException: If there's an error executing the procedure
            ValueError: If procedure_name is empty or invalid parameters provided
        """
        if not procedure_name or not procedure_name.strip():
            raise ValueError("Procedure name cannot be empty")

        try:
            query = self._build_procedure_query(procedure_name, parameters)
            return self.__cnxn.query(query, stats=stats)

        except Exception as e:  # pragma: no cover
            return self._handle_procedure_error(e)

    def query(self,
              query: str,
              stats: bool = False
              ):
        try:
            return self.__cnxn.query(query, stats=stats)
        except Exception as e:
            raise SQLQueryException(f'{e}')

    def close(self):
        try:
            self.__cnxn.close()
        except Exception as e:
            raise SQLConnectionException(f'{e}')

    def _logging(self):
        oSqlDriver = {
            "driver-info":
                {
                    "using": self.__driverOverride,
                    "odbc": {
                        "loaded": self.__odbcLoaded,
                        "driver": self.__odbcDriver
                    },
                    "jdbc": {
                        "loaded": self.__jdbcLoaded,
                        "driver": self.__jdbcDriver
                    }
                }
        }
        self.__connStatus = json.dumps(oSqlDriver)

    @staticmethod
    def _build_procedure_query(procedure_name: str, parameters: Optional[Dict[str, Any]]) -> str:
        """
        Builds the SQL query string for executing a stored procedure.

        Args:
            procedure_name: Name of the stored procedure
            parameters: Dictionary of parameter names and values

        Returns:
            Formatted SQL query string
        """
        PARAM_PREFIX = '@'
        query_parts = [f"EXEC {procedure_name}"]

        if parameters:
            param_expressions = []
            for name, value in parameters.items():
                formatted_value = f"'{value}'" if isinstance(value, str) else str(value)
                param_expressions.append(f"{PARAM_PREFIX}{name}={formatted_value}")

            if param_expressions:
                query_parts.append(" " + ", ".join(param_expressions))

        return "".join(query_parts)

    def _handle_procedure_error(self, error: Exception) -> Optional[Dict[str, Any]]:
        """
        Handles errors that occur during stored procedure execution.

        Args:
            error: The caught exception

        Returns:
            None if no meaningful error data found

        Raises:
            SQLQueryException: If a database-specific error is found
        """
        try:
            error_data = json.loads(str(error))
            # --> logger.warning(f"error_data: {error_data}")
            match self.__driverOverride:
                case "odbc":
                    odbc_error = self._get_query_error(error_data, ODBC_QUERY_KEY)
                    # odbc_error = None if 'No results' not in error_data else self.parse_sql_error(odbc_error)
                    odbc_error = self._parse_sql_error(odbc_error)
                    # --> logger.success(f"odbc_error: {odbc_error}")
                    if odbc_error:
                        raise SQLQueryException(json.dumps(odbc_error, indent=2))

                case "jdbc":
                    jdbc_error = self._get_query_error(error_data, JDBC_QUERY_KEY)
                    jdbc_error = None if jdbc_error == "" else self._parse_jdbc_error(jdbc_error)
                    # --> logger.success(f"jdbc_error: {jdbc_error}")
                    if jdbc_error:
                        raise SQLQueryException(json.dumps(jdbc_error, indent=2))

        except json.JSONDecodeError:
            # Not a JSON-formatted error, return None
            pass

        return None

    @staticmethod
    def _get_query_error(query_results: Dict[str, Any], query_type: str) -> Optional[str]:
        """
        Extract an error message from query results for a specific query type.

        Args:
            query_results: Dictionary containing query execution results
            query_type: Type of query (e.g., 'jdbc-query', 'odbc-query')

        Returns:
            Error message if present, None otherwise
        """
        return query_results.get(query_type, {}).get(ERROR_KEY)

    @staticmethod
    def _parse_sql_error(error_string: str) -> None | dict:
        """
        Converts a SQL Server error string into a structured JSON object.

        Args:
            error_string: A tuple string containing error code and message
                Format 1: ('code', '[state] [Provider][Component][Source]Message (ErrorNum) (Context)')
                Format 2: ('code', '[state] [Provider][Component]Message (ErrorNum) (Context)')

        Returns:
            dict: A structured JSON object containing parsed error information

        Examples:
            >> error1 = "('42000', '[42000] [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Procedure or function 'sp_TestNoResults' expects parameter '@quality', which was not supplied. (201) (SQLExecDirectW)')"
            >> error2 = "('07002', '[07002] [Microsoft][ODBC Driver 18 for SQL Server]COUNT field incorrect or syntax error (0) (SQLExecDirectW)')"
            >> parse_sql_error(error1)
            >> parse_sql_error(error2)
        """
        if not error_string:
            return {"errorCode": "UNKNOWN", "errorMessage": "No error message found."}

        if 'No results' in error_string:
            return None

        # Try the three-part format first (with [SQL Server])
        pattern1 = r"\('(\d+)',\s*'\[(\d+)\]\s*\[([^\]]+)\]\[([^\]]+)\]\[([^\]]+)\]([^(]+)\((\d+)\)\s*\(([^)]+)\)'\)"
        # Try the two-part format (without [SQL Server])
        pattern2 = r"\('(\d+)',\s*'\[(\d+)\]\s*\[([^\]]+)\]\[([^\]]+)\]([^(]+)\((\d+)\)\s*\(([^)]+)\)'\)"

        match = re.match(pattern1, error_string.replace('"', "'"))

        if match:
            error_obj = {
                "errorCode": match.group(1),
                "errorMessage": {
                    "sqlState": match.group(2),
                    "provider": {
                        "name": match.group(3).strip(),
                        "component": match.group(4).strip(),
                        "source": match.group(5).strip()
                    },
                    "error": match.group(6).strip(),
                    "errorNumber": match.group(7),
                    "context": match.group(8)
                }
            }
        else:
            match = re.match(pattern2, error_string.replace('"', "'"))
            if match:
                error_obj = {
                    "errorCode": match.group(1),
                    "errorMessage": {
                        "sqlState": match.group(2),
                        "provider": {
                            "name": match.group(3).strip(),
                            "component": match.group(4).strip(),
                            "source": "SQL Server"  # Default value when not in the message
                        },
                        "error": match.group(5).strip(),
                        "errorNumber": match.group(6),
                        "context": match.group(7)
                    }
                }
            else:
                return {"errorCode": "UNKNOWN", "errorMessage": error_string}

        return error_obj

    @staticmethod
    def _parse_jdbc_error(error_string: str) -> dict:
        """
        Converts a JDBC SQLServer exception string into a structured JSON object.

        Args:
            error_string: A string containing the JDBC exception
                Format: 'com.microsoft.sqlserver.jdbc.SQLServerException: Error message'

        Returns:
            dict: A structured JSON object containing parsed error information

        Example:
            >> error = "com.microsoft.sqlserver.jdbc.SQLServerException: Could not find stored procedure 'sp_TestNoResult'."
            >> _parse_jdbc_error(error)
        """
        if not error_string:
            return {"errorCode": "UNKNOWN", "errorMessage": "No error message found."}

        pattern = r"(?P<exception_class>com\.microsoft\.sqlserver\.jdbc\.\w+):\s*(?P<message>.*)"
        match = re.match(pattern, error_string)

        if not match:
            return {"errorCode": "UNKNOWN", "errorMessage": error_string}

        error_obj = {
            "errorCode": "JDBC_ERROR",
            "errorMessage": {
                "sqlState": "JDBC",
                "provider": {
                    "name": "Microsoft",
                    "component": "JDBC Driver",
                    "source": "SQL Server"
                },
                "error": match.group("message").strip(),
                "errorNumber": "0",  # JDBC doesn't provide an error number in this format
                "context": match.group("exception_class")
            }
        }

        return error_obj

    @property
    def isConnected(self) -> bool:
        return self.__isConnected
        # return True

    @property
    def ConnectedStatus(self):

        return self.__connStatus

    @property
    def ConnectedStats(self):

        return self.__connStats

    @property
    def version(self):
        return __version__
