"""PostgreSQL dialect handler for SQL code generation."""
from typing import Tuple

from sqlalchemy import dialects, Table

from .base import DialectHandler
from .utils import get_columns_list


class PostgresDialectHandler(DialectHandler):
    """Dialect handler for PostgreSQL."""

    def get_boolean_type(self):
        return dialects.postgresql.BOOLEAN

    def get_proc_name_format(self, schema: str, operation: str, entity_name: str) -> str:
        """Get PostgreSQL procedure naming format."""
        return f'"{schema}"."{operation}_{entity_name}"'

    def apply_proc_template(self, proc_name: str, sql_body: str, header: str) -> str:
        """Wrap SQL body in PostgreSQL procedure profile with error handling."""
        # language=sql
        proc_template_sql = f"""
{header}
CREATE OR REPLACE PROCEDURE {proc_name}(p_parent_executionID BIGINT DEFAULT NULL)
LANGUAGE plpgsql
AS $$
DECLARE
    v_executionID BIGINT;
    v_ErrorMessage TEXT;
    v_ErrorDetail TEXT;
    v_ErrorContext TEXT;
    v_ErrorID INT;
BEGIN
    -- Log execution start
    CALL core."LogExecution"(0::INT, NULL::BIGINT, v_executionID, p_parent_executionID);

    BEGIN
        {sql_body}

        -- Log execution end
        CALL core."LogExecution"(0::INT, v_executionID, v_executionID, NULL::BIGINT);
    EXCEPTION WHEN OTHERS THEN
        GET STACKED DIAGNOSTICS
            v_ErrorMessage = MESSAGE_TEXT,
            v_ErrorDetail = PG_EXCEPTION_DETAIL,
            v_ErrorContext = PG_EXCEPTION_CONTEXT;

        -- Log error
        INSERT INTO core."ErrorLog" (
            "UserName",
            "ErrorNumber",
            "ErrorState",
            "ErrorSeverity",
            "ErrorLine",
            "ErrorProcedure",
            "ErrorMessage",
            "ErrorDateTime"
        ) VALUES (
            current_user,
            NULL,
            NULL,
            NULL,
            NULL,
            {proc_name}::TEXT,
            v_ErrorMessage || COALESCE(' | ' || v_ErrorDetail, ''),
            CURRENT_TIMESTAMP
        ) RETURNING "ErrorID" INTO v_ErrorID;

        -- Update execution log with error
        UPDATE core."ExecutionLog"
        SET "errorID" = v_ErrorID,
            "end_timestamp" = CURRENT_TIMESTAMP
        WHERE "executionID" = v_executionID;

        -- Re-raise the error
        RAISE;
    END;
END;
$$;
"""
        return proc_template_sql

    def make_stg_materialization_proc(self, entity_name: str, header: str) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Populate_stg", entity_name)

        # language=sql
        proc_body = f"""
        DROP TABLE IF EXISTS "stg"."{entity_name}";
        CREATE TABLE "stg"."{entity_name}" AS
        SELECT * FROM "proxy"."{entity_name}";
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_hub_proc(self, hub_table: Table, bk_keys: list, header: str) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Populate_{hub_table.schema}", hub_table.name)
        where_fields_list_str = " AND ".join([f'hub."{bk[0]}" = stg."{bk[0]}"' for bk in bk_keys])

        # Convert columns list to use double quotes
        columns_list = ", ".join([f'"{col.name}"' for col in hub_table.columns.values()])
        columns_list_stg = ", ".join([f'stg."{col.name}"' for col in hub_table.columns.values()])

        # language=sql
        proc_body = f"""
        INSERT INTO "{hub_table.schema}"."{hub_table.name}"
        ({columns_list})
        SELECT DISTINCT {columns_list_stg}
        FROM "stg"."{hub_table.name}" AS stg
        WHERE NOT EXISTS (
            SELECT 1
            FROM "{hub_table.schema}"."{hub_table.name}" AS hub
            WHERE {where_fields_list_str}
        );
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_link_proc(self, link_table: Table, hk_keys: list, header: str) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Populate_{link_table.schema}", link_table.name)
        where_fields_list_str = "\n\t\t\tAND ".join([f'link."{hk[0]}" = stg."{hk[0]}"' for hk in hk_keys if hk[0] != f"hk_{link_table.name}"])

        # Convert columns list to use double quotes
        columns_list = ", ".join([f'"{col.name}"' for col in link_table.columns.values()])
        columns_list_stg = ", ".join([f'stg."{col.name}"' for col in link_table.columns.values()])

        # language=sql
        proc_body = f"""
        INSERT INTO "{link_table.schema}"."{link_table.name}"
        ({columns_list})
        SELECT DISTINCT {columns_list_stg}
        FROM "stg"."{link_table.name}" AS stg
        WHERE NOT EXISTS (
            SELECT 1
            FROM "{link_table.schema}"."{link_table.name}" AS link
            WHERE {where_fields_list_str}
        );
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_scd0_sat_proc(self, sat_table: Table, header: str) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Populate_{sat_table.schema}", sat_table.name)
        hk_name = f"hk_{sat_table.name}"

        # Convert columns list to use double quotes
        columns_list = ", ".join([f'"{col.name}"' for col in sat_table.columns.values()])
        columns_list_stg = ", ".join([f'stg."{col.name}"' for col in sat_table.columns.values()])

        # language=sql
        proc_body = f"""
        INSERT INTO "{sat_table.schema}"."{sat_table.name}"
        ({columns_list})
        SELECT {columns_list_stg}
        FROM "stg"."{sat_table.name}" stg
        WHERE NOT EXISTS (
            SELECT 1
            FROM "sat"."{sat_table.name}" sat
            WHERE stg."{hk_name}" = sat."{hk_name}"
        );
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_scd2_sat_proc(
        self,
        sat_table: Table,
        hk_name: str,
        hashdiff_col: str,
        is_available_col: str,
        loaddate_col: str,
        stg_schema: str,
        header: str
    ) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Populate_{sat_table.schema}", sat_table.name)
        columns_list = ", ".join([f'"{col.name}"' for col in sat_table.columns.values()])

        def smart_replace(column_name: str) -> str:
            if column_name == "LoadDate":
                result = 'CURRENT_TIMESTAMP AS "LoadDate"'
            elif column_name == "IsAvailable":
                result = 'FALSE AS "IsAvailable"'
            else:
                result = f'sat."{column_name}"'
            return result

        select_columns_list = ", ".join([smart_replace(col.name) for col in sat_table.columns.values()])
        columns_list_stg = ", ".join([f'stg."{col.name}"' for col in sat_table.columns.values()])

        if stg_schema == "proxy":
            stg_table_name = f'"stg"."{sat_table.name}"'
            materialization_stmt = ""
        else:
            stg_table_name = "materialized_temp"
            materialization_stmt = f"""
        CREATE TEMPORARY TABLE materialized_temp AS
        SELECT DISTINCT {columns_list}
        FROM "stg"."{sat_table.name}";
"""

        # language=sql
        proc_body = f"""{materialization_stmt}
        WITH ranked_history AS (
            SELECT {columns_list},
                ROW_NUMBER() OVER (PARTITION BY "{hk_name}" ORDER BY "{loaddate_col}" DESC) AS "DescRank"
            FROM "{sat_table.schema}"."{sat_table.name}"
        )
        INSERT INTO "{sat_table.schema}"."{sat_table.name}"
        ({columns_list})
        SELECT {columns_list_stg}
        FROM {stg_table_name} stg
        WHERE NOT EXISTS (
            SELECT 1
            FROM ranked_history sat
            WHERE sat."DescRank" = 1
                AND stg."{hk_name}" = sat."{hk_name}"
                AND stg."{hashdiff_col}" = sat."{hashdiff_col}"
                AND sat."{is_available_col}" = TRUE
        )

        UNION ALL

        SELECT {select_columns_list}
        FROM ranked_history sat
        WHERE NOT EXISTS (
            SELECT 1
            FROM {stg_table_name} stg
            WHERE stg."{hk_name}" = sat."{hk_name}"
        )
        AND sat."DescRank" = 1
        AND sat."{is_available_col}" = TRUE;

        {f'DROP TABLE IF EXISTS {stg_table_name};' if stg_schema != "proxy" else ''}
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_scd2_dim_proc(self, dim_table: Table, bk_keys: list, header: str) -> Tuple[str, str, str]:
        proc_name = self.get_proc_name_format("elt", f"Recalculate_{dim_table.schema}", dim_table.name)
        columns_list = ", ".join([f'"{col.name}"' for col in dim_table.columns.values()])
        pk_keys = lambda: ", ".join([f'sat."{bk[0]}"' for bk in bk_keys])

        def smart_replace(column_name: str) -> str:
            if column_name == "DateFrom":
                result = 'sat."LoadDate" AS "DateFrom"'
            elif column_name == "DateTo":
                result = f'LEAD(sat."LoadDate" - INTERVAL \'1 microsecond\', 1, \'9999-12-31 23:59:59.999999\'::TIMESTAMP) OVER (PARTITION BY {pk_keys()} ORDER BY sat."LoadDate") AS "DateTo"'
            elif column_name == "IsCurrent":
                result = f'CASE WHEN LEAD(sat."LoadDate") OVER (PARTITION BY {pk_keys()} ORDER BY sat."LoadDate") IS NULL THEN TRUE ELSE FALSE END AS "IsCurrent"'
            else:
                result = f'sat."{column_name}"'
            return result

        select_columns_list = ",\n\t\t".join([smart_replace(col.name) for col in dim_table.columns.values()])

        # language=sql
        proc_body = f"""
        TRUNCATE TABLE "{dim_table.schema}"."{dim_table.name}";

        INSERT INTO "{dim_table.schema}"."{dim_table.name}"
        ({columns_list})
        SELECT {select_columns_list}
        FROM "sat"."{dim_table.name}" sat;
"""
        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_job_proc(self, entity_name: str, proc_names: list[str], header: str) -> Tuple[str, str, str]:
        proc_name = f'"job"."Run_all_related_to_{entity_name}"'
        proc_body = "\n\t\t"
        for proc in proc_names:
            if proc is None:
                continue
            proc_body += f"CALL {proc}(v_executionID);\n\t\t"

        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"

    def make_drop_proc(self, entity_name, table_schemas: list[str], procedures: list[str], header: str) -> Tuple[str, str, str]:
        proc_name = f'"meta"."Drop_all_related_to_{entity_name}"'
        proc_body = "\n\t\t"
        for proc in procedures:
            if proc is None:
                continue
            proc_body += f"DROP PROCEDURE IF EXISTS {proc};\n\t\t"
        proc_body += "\n\t\t"
        for schema in table_schemas:
            proc_body += f'DROP TABLE IF EXISTS "{schema}"."{entity_name}";\n\t\t'
        proc_body += "\n\t\t"
        proc_body += f"""UPDATE core."entities"
        SET "deleted" = CURRENT_TIMESTAMP,
            "is_deleted" = TRUE
        WHERE "entity_name" = '{entity_name}';
"""

        proc_code = self.apply_proc_template(proc_name, proc_body, header)
        return proc_code, proc_name, f"CALL {proc_name}()"
