from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError

from private_gpt.components.database.inspected_schema import (
    InspectedFunction,
    InspectedFunctionParams,
)
from private_gpt.components.database.inspector_interface import (
    DatabaseObjectInspector,
    DatabaseObjectType,
)


class DatabaseFunctionsInspector(DatabaseObjectInspector):
    def get_inspector_type(self) -> str:
        return DatabaseObjectType.FUNCTION

    def get_objects(self, schema: str) -> list[InspectedFunction]:  # type: ignore[override]
        try:
            if self._db_type in ["mssql", "microsoft"]:
                return self._get_sqlserver_functions(schema)
            if self._db_type in ["db2", "ibm_db_sa"]:
                return self._get_db2_functions(schema)

            # TODO: Postgres support is not complete
            # elif self._db_type == "postgresql":
            #    return self._get_postgresql_functions(schema)
            else:
                return []
        except SQLAlchemyError:
            return []

    def _get_postgresql_functions(self, schema: str) -> list[InspectedFunction]:
        # TODO: review this function (it is not finished)
        self._ensure_connected()
        schema_filter = f"AND n.nspname = '{schema}'" if schema else ""
        if self._is_readonly:
            schema_filter += " AND n.nspname NOT IN ('pg_temp_1', 'pg_toast_temp_1')"
            schema_filter += " AND UPPER(pg_get_functiondef(p.oid)) NOT LIKE '%DELETE%'"
            schema_filter += " AND UPPER(pg_get_functiondef(p.oid)) NOT LIKE '%UPDATE%'"
            schema_filter += " AND UPPER(pg_get_functiondef(p.oid)) NOT LIKE '%INSERT%'"
            schema_filter += " AND UPPER(pg_get_functiondef(p.oid)) NOT LIKE '%DROP%'"
            schema_filter += " AND UPPER(pg_get_functiondef(p.oid)) NOT LIKE '%ALTER%'"

        types = "('f')"  # To include only functions

        query = text(
            f"""
                SELECT
                    n.nspname as schema_name,
                    p.proname as procedure_name,
                    pg_get_function_result(p.oid) as return_type,
                    params.param_name,
                    format_type(params.param_type, NULL) as param_type_name,
                    params.param_position
                FROM pg_proc p
                JOIN pg_namespace n ON p.pronamespace = n.oid
                CROSS JOIN LATERAL (
                    SELECT *
                    FROM ROWS FROM (
                        unnest(COALESCE(p.proargnames, ARRAY_FILL(NULL::text, ARRAY[p.pronargs]))),
                        unnest(COALESCE(p.proallargtypes, p.proargtypes::oid[])),
                        generate_series(1, GREATEST(
                            array_length(COALESCE(p.proallargtypes, p.proargtypes::oid[]), 1),
                            p.pronargs
                        ))
                    ) AS t(param_name, param_type, param_position)
                    WHERE t.param_type IS NOT NULL
                ) params
                WHERE p.prokind IN {types}
                  {schema_filter}
                AND n.nspname NOT IN ('pg_catalog', 'information_schema')
                ORDER BY n.nspname, p.proname, params.param_position
            """
        )

        functions = []
        try:
            if not self._engine:
                return []
            conn = self._engine.connect()
            result = conn.execute(query)
            current_function = None

            for row in result:
                schema, name, return_type, param_name, param_type, param_position = (
                    row[0],
                    row[1],
                    row[2],
                    row[3],
                    row[4],
                    row[5],
                )
                if current_function is None or (
                    current_function.schema != schema or current_function.name != name
                ):
                    if current_function is not None:
                        functions.append(current_function)
                    current_function = InspectedFunction()
                    current_function.schema = schema
                    current_function.name = name
                    current_function.return_types = [return_type]
                    current_function.parameters = []
                if (
                    (param_name or param_position)
                    and current_function
                    and current_function.parameters
                ):
                    parameter = InspectedFunctionParams()
                    parameter.name = (
                        param_name if param_name else f"arg{param_position}"
                    )
                    parameter.data_type = param_type
                    current_function.parameters.append(parameter)

            if current_function is not None:
                functions.append(current_function)
        except Exception:
            return []
        return functions

    def _get_sqlserver_functions(self, schema: str) -> list[InspectedFunction]:

        schema_filter = f"AND s.name = '{schema}'" if schema else ""
        if self._is_readonly:
            schema_filter += "AND UPPER(m.definition) NOT LIKE '%DELETE%'"
            schema_filter += "AND UPPER(m.definition) NOT LIKE '%UPDATE%'"
            schema_filter += "AND UPPER(m.definition) NOT LIKE '%INSERT%'"
            schema_filter += "AND UPPER(m.definition) NOT LIKE '%DROP%'"
            schema_filter += "AND UPPER(m.definition) NOT LIKE '%ALTER%'"
        types = "('FN')"  # Only stored functions
        query = text(
            f"""
                SELECT
                    s.name AS schema_name,
                    o.name AS object_name,
                    CAST(ep_proc.value AS NVARCHAR(MAX)) AS comment,
                    prm.name AS parameter_name,
                    t.name AS parameter_type,
                    CAST(ep_param.value AS NVARCHAR(MAX)) AS parameter_comment
                FROM sys.objects o
                    JOIN sys.schemas s ON o.schema_id = s.schema_id
                    LEFT JOIN sys.parameters prm ON o.object_id = prm.object_id
                    LEFT JOIN sys.types t ON prm.user_type_id = t.user_type_id
                    JOIN sys.sql_modules m ON o.object_id = m.object_id
                    LEFT JOIN sys.extended_properties ep_proc ON
                        ep_proc.major_id = o.object_id
                        AND ep_proc.minor_id = 0
                        AND ep_proc.name = 'MS_Description'
                        AND ep_proc.class = 1
                    LEFT JOIN sys.extended_properties ep_param ON
                        ep_param.major_id = o.object_id
                        AND ep_param.minor_id = prm.parameter_id
                        AND ep_param.name = 'MS_Description'
                        AND ep_param.class = 2
                WHERE o.type IN {types}
                  {schema_filter}
                  ORDER BY s.name, o.name;
            """
        )

        functions = []
        try:
            self._ensure_connected()
            if not self._engine:
                return []
            conn = self._engine.connect()
            result = conn.execute(query)
            current_function = None

            for row in result:
                schema, name, comment, param_name, param_type, parameter_comment = (
                    row[0],
                    row[1],
                    row[2],
                    row[3],
                    row[4],
                    row[5],
                )
                if current_function is None or (
                    current_function.schema != schema or current_function.name != name
                ):
                    if current_function is not None:
                        functions.append(current_function)
                    current_function = InspectedFunction()
                    current_function.schema = schema
                    current_function.name = name
                    current_function.comment = comment
                    current_function.parameters = []
                    current_function.return_types = []
                if param_name:
                    parameter = InspectedFunctionParams()
                    parameter.name = param_name
                    parameter.data_type = param_type
                    parameter.comment = parameter_comment
                    if not current_function.parameters:
                        current_function.parameters = []
                    current_function.parameters.append(parameter)
                elif param_type:
                    if not current_function.return_types:
                        current_function.return_types = []
                    current_function.return_types.append(param_type)

            if current_function is not None:
                functions.append(current_function)
        except Exception:
            return []
        return functions

    def _get_db2_functions(self, schema: str) -> list[InspectedFunction]:
        schema_filter = (
            f"AND LOWER(r.routineschema) = LOWER('{schema}')" if schema else ""
        )

        if self._is_readonly:
            schema_filter += " AND UPPER(r.text) NOT LIKE '%DELETE%'"
            schema_filter += " AND UPPER(r.text) NOT LIKE '%UPDATE%'"
            schema_filter += " AND UPPER(r.text) NOT LIKE '%INSERT%'"
            schema_filter += " AND UPPER(r.text) NOT LIKE '%DROP%'"
            schema_filter += " AND UPPER(r.text) NOT LIKE '%ALTER%'"

        # Only stored functions (F = Function)
        query = text(
            f"""
                SELECT
                    r.routineschema AS schema_name,
                    r.routinename AS object_name,
                    r.remarks AS comment,
                    p.parmname AS parameter_name,
                    p.typename AS parameter_type,
                    p.rowtype AS param_direction
                FROM syscat.routines r
                    LEFT JOIN syscat.routineparms p ON
                        r.routineschema = p.routineschema
                        AND r.specificname = p.specificname
                WHERE r.routinetype = 'F'
                  AND r.routineschema NOT LIKE 'SYS%'
                  {schema_filter}
                ORDER BY r.routineschema, r.routinename, p.ordinal
            """
        )

        functions = []
        try:
            self._ensure_connected()
            if not self._engine:
                return []
            conn = self._engine.connect()
            result = conn.execute(query)
            current_function = None

            for row in result:
                (schema, name, comment, param_name, param_type, param_direction,) = (
                    row[0],
                    row[1],
                    row[2],
                    row[3],
                    row[4],
                    row[5],
                )

                if current_function is None or (
                    current_function.schema != schema or current_function.name != name
                ):
                    if current_function is not None:
                        functions.append(current_function)
                    current_function = InspectedFunction()
                    current_function.schema = schema
                    current_function.name = name
                    current_function.comment = comment
                    current_function.parameters = []
                    current_function.return_types = []

                if param_name and param_direction in ("P", "O", "B"):
                    # P = IN, O = OUT, B = INOUT
                    parameter = InspectedFunctionParams()
                    parameter.name = param_name
                    parameter.data_type = param_type
                    parameter.comment = None
                    current_function.parameters.append(parameter)
                elif param_direction == "R" and param_type:
                    current_function.return_types.append(
                        f"{param_name if param_name else''} {param_type}"
                    )

            if current_function is not None:
                functions.append(current_function)
        except Exception:
            return []
        return functions
