from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Callable, Tuple

from sqlalchemy import Table, MetaData, Column, UniqueConstraint

from sandwich import SANDWICH_VERSION
from sandwich.dialects import DialectHandler
from sandwich.modeling import Dv2SystemInfo, modeling_metadata, StgInfo

@dataclass(frozen=True)
class ValidationResult:
    stg_schema: str
    entity_name: str
    bk_keys: list[Tuple[str, Any]]
    hk_keys: list[Tuple[str, Any]]
    business_column_types: dict[str, Any]
    system_column_types: dict[str, Any]
    template: str
    degenerate_field: Tuple[str, Any] | None = None

class Validator(ABC):
    @abstractmethod
    def validate_staging(self, stg_info: StgInfo, sys_info: Dv2SystemInfo, verbose: bool = False) -> ValidationResult:
        pass

class BaseValidator(Validator):
    def __init__(self, template: str):
        self._on_validate_staging: Callable[[StgInfo, Dv2SystemInfo], None] | None = None
        self._on_hub_validate_staging: Callable[[StgInfo, Dv2SystemInfo], None] | None = None
        self.template = template

    def validate_staging(self, stg_info: StgInfo, sys_info: Dv2SystemInfo, verbose: bool = False) -> ValidationResult:
        """Raises: Exception"""
        if verbose:
            raise Exception("verbose is not implemented yet")

        system_column_names = stg_info.sys_columns.keys()

        # universal check - all dv2 raw objects should be auditable
        for required_col in modeling_metadata.required_columns:
            if required_col not in system_column_names:
                raise Exception(f"{required_col} column is required")

        if self._on_hub_validate_staging is not None:
            self._on_hub_validate_staging(stg_info, sys_info)

        if self._on_validate_staging is not None:
            self._on_validate_staging(stg_info, sys_info)

        # todo: ValidationResult is not required whatsoever
        return ValidationResult(
            stg_schema=stg_info.stg_schema,
            entity_name=stg_info.stg_name,
            bk_keys=[(nm, tp) for nm, tp in stg_info.bk_keys.items()],
            hk_keys=[(nm, tp) for nm, tp in stg_info.hk_keys.items()],
            degenerate_field = stg_info.degenerate_field,
            business_column_types=stg_info.bus_columns,
            system_column_types=stg_info.sys_columns,
            template=self.template
        )

class HubBasedValidator(BaseValidator):
    def __init__(self, template: str):
        super().__init__(template)
        self._on_hub_validate_staging = self._validate_hub_staging

    @staticmethod
    def _validate_hub_staging(stg_info: StgInfo, sys_info: Dv2SystemInfo) -> None:
        # -----------------
        # hk
        # -----------------
        # only one hash key is allowed for a hub table
        # and its name should match `hk_[entity_name]` pattern
        hk_count = len(stg_info.hk_keys)
        if hk_count == 0:
            raise Exception("hk column is required for `scd2dim` validation")
        elif hk_count > 1:
            raise Exception(f"More than one hk column found in stg.{stg_info.stg_name}")
        # hk_key = (key_name, key_type)
        hk_key = list(stg_info.hk_keys.items())[0]
        if hk_key[0] != f"hk_{stg_info.stg_name}":
            raise Exception(f"hk column has invalid name '{hk_key[0]}'")

class SchemaGenerator(ABC):
    @abstractmethod
    def make_tables(self) -> dict[str, Table]:
        pass

    @abstractmethod
    def make_procedures(self, tables: dict[str, Table]) -> dict[str, Tuple[str, str, str]]:
        pass

class BaseSchemaGenerator(SchemaGenerator):
    def __init__(self, dialect_handler: DialectHandler, validation_result: ValidationResult, entity_registration_date: datetime):
        self.dialect_handler = dialect_handler
        self._validation_result = validation_result
        self.header = modeling_metadata.HEADER_TEMPLATE.format(
            created_on=entity_registration_date,
            updated_on=datetime.now(),
            version=SANDWICH_VERSION,
            entity_name=self._validation_result.entity_name
        )
        self._on_make_procedures: list[Callable[[dict[str, Tuple[str, str, str]], dict[str, Table]], None] | None] = []

    @abstractmethod
    def make_tables(self) -> dict[str, Table]:
        pass

    def make_procedures(self, tables: dict[str, Table]) -> dict[str, Tuple[str, str, str]]:
        procedures: dict[str, Tuple[str, str, str]] = {}

        # extension point
        for on_make_proc in self._on_make_procedures:
            on_make_proc(procedures, tables)

        proc_names = [tup[1] for tup in procedures.values()]
        table_schemas = list(procedures.keys())

        # job procedure
        job_proc_code, job_proc_name, job_call_stmt = self.dialect_handler.make_job_proc(
            entity_name=self._validation_result.entity_name,
            proc_names=proc_names,
            header=self.header
        )
        procedures["job"] = (job_proc_code, job_proc_name, job_call_stmt)
        proc_names.append(job_proc_name)

        # drop procedure
        drop_proc_code, drop_proc_name, drop_call_stmt = self.dialect_handler.make_drop_proc(
            entity_name=self._validation_result.entity_name,
            table_schemas=table_schemas,
            procedures=proc_names,
            header=self.header
        )
        procedures["drop"] = (drop_proc_code, drop_proc_name, drop_call_stmt)

        return procedures


class HubSchemaGenerator(BaseSchemaGenerator):
    def __init__(self, dialect_handler: DialectHandler, validation_result: ValidationResult, entity_registration_date: datetime):
        super().__init__(dialect_handler, validation_result, entity_registration_date)
        self._on_make_procedures.append(self._make_hub_proc)

    def make_tables(self) -> dict[str, Table]:
        return {
            "hub": self.make_hub_table(),
        }

    def make_hub_table(self) -> Table:
        entity_name = self._validation_result.entity_name

        hub_table = Table(entity_name, MetaData(), schema="hub")
        uks: list[str] = []

        # BKs
        for bk_key in self._validation_result.bk_keys:
            uks.append(bk_key[0])
            bk_col = Column(bk_key[0], bk_key[1], nullable=False)
            hub_table.append_column(bk_col)
        hub_table.append_constraint(UniqueConstraint(*uks))

        # HK
        hk_key = self._validation_result.hk_keys[0]
        hub_table.append_column(Column(hk_key[0], hk_key[1], primary_key=True))

        # LoadDate
        load_date = modeling_metadata.loaddate
        load_date_type = self._validation_result.system_column_types[load_date]
        load_date_col = Column(load_date, load_date_type, nullable=False)
        hub_table.append_column(load_date_col)

        # RecordSource
        record_source = modeling_metadata.recordsource
        record_source_type = self._validation_result.system_column_types[record_source]
        record_source_col = Column(record_source, record_source_type, nullable=False)
        hub_table.append_column(record_source_col)

        return hub_table

    def _make_hub_proc(self, procedures: dict[str, Tuple[str, str, str]], tables: dict[str, Table]) -> None:
        hub_proc_code, hub_proc_name, hub_call_stmt = self.dialect_handler.make_hub_proc(
            hub_table=tables["hub"],
            bk_keys=self._validation_result.bk_keys,
            header=self.header
        )
        procedures["hub"] = (hub_proc_code, hub_proc_name, hub_call_stmt)