from datetime import datetime
from typing import Tuple, Any

from sqlalchemy import Table, MetaData, Column, UniqueConstraint

from sandwich import SANDWICH_VERSION
from sandwich.dialects import DialectHandler
from sandwich.modeling.dataclasses import ValidationResult
from sandwich.modeling.metadata import modeling_metadata

from .schema_generator import SchemaGenerator


#todo: should be derived from `BaseSchemaGenerator`
class LinkSat0SchemaGenerator(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
        )

    def make_tables(self) -> dict[str, Table]:
        return {
            "link": self.make_link_table(),
            "sat": self.make_sat_table(),
            #"fact": self.make_fact_table(),
        }

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

        # Create link table
        link_table = Table(entity_name, MetaData(), schema="link")
        uks: list[str] = []

        # HKs (own and FKs)
        for hk_key in self._validation_result.hk_keys:
            if hk_key[0] == f"hk_{entity_name}":
                col = Column(hk_key[0], hk_key[1], primary_key=True)
            else:
                uks.append(hk_key[0])
                col = Column(hk_key[0], hk_key[1], nullable=False)
            link_table.append_column(col)

        for (dg_name, dg_type) in self._validation_result.degenerate_field.items():
            link_table.append_column(Column(dg_name, dg_type, nullable=False))
            uks.append(dg_name)

        link_table.append_constraint(UniqueConstraint(*uks))

        # 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)
        link_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)
        link_table.append_column(record_source_col)

        return link_table

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

        # Create sat table
        sat_table = Table(entity_name, MetaData(), schema="sat")

        # own HK
        for hk_key in self._validation_result.hk_keys:
            if hk_key[0] == f"hk_{entity_name}":
                col = Column(hk_key[0], hk_key[1], primary_key=True)
                sat_table.append_column(col)

        # 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)
        sat_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)
        sat_table.append_column(record_source_col)

        # for transactional links
        for (dg_name, dg_type) in self._validation_result.degenerate_field.items():
            sat_table.append_column(Column(dg_name, dg_type, nullable=False))

        for (name_, type_) in self._validation_result.business_column_types.items():
            col = Column(name_, type_, nullable=True)
            sat_table.append_column(col)

        return sat_table

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

        # Create a fact table
        fact_table = Table(entity_name, MetaData(), schema="fact")

        # not own HKs only
        for hk_key in self._validation_result.hk_keys:
            if hk_key[0] != f"hk_{entity_name}":
                col = Column(hk_key[0], hk_key[1], primary_key=True)
                fact_table.append_column(col)

        for (name_, type_) in self._validation_result.business_column_types.items():
            col = Column(name_, type_, nullable=True)
            fact_table.append_column(col)

        return fact_table

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

        stg_proc_name = None
        if self._validation_result.stg_schema == "proxy":
            stg_proc_code, stg_proc_name, stg_call_stmt = self.dialect_handler.make_stg_materialization_proc(
                entity_name=self._validation_result.entity_name,
                header=self.header
            )
            procedures["stg"] = (stg_proc_code, stg_proc_name, stg_call_stmt)

        link_table = tables["link"]
        dg_keys: list[Tuple[str, Any]] = [(k, v) for k, v in self._validation_result.degenerate_field.items()]
        link_proc_code, link_proc_name, link_call_stmt = self.dialect_handler.make_link_proc(
            link_table=link_table,
            hk_keys=self._validation_result.hk_keys + dg_keys,
            header=self.header
        )
        procedures["link"] = (link_proc_code, link_proc_name, link_call_stmt)

        sat_table = tables["sat"]
        sat_proc_code, sat_proc_name, sat_call_stmt = self.dialect_handler.make_scd0_sat_proc(
            sat_table=sat_table,
            header=self.header
        )
        procedures["sat"] = (sat_proc_code, sat_proc_name, sat_call_stmt)

        # job procedure
        job_proc_names = [] # order-sensitive
        if self._validation_result.stg_schema == "proxy":
            job_proc_names.append(stg_proc_name)
        job_proc_names.extend([link_proc_name, sat_proc_name])
        job_proc_code, job_proc_name, job_call_stmt = self.dialect_handler.make_job_proc(
            entity_name=self._validation_result.entity_name,
            proc_names=job_proc_names,
            header=self.header
        )
        procedures["job"] = (job_proc_code, job_proc_name, job_call_stmt)

        # drop procedure
        drop_table_schemas = ["link", "sat"]
        if self._validation_result.stg_schema == "proxy":
            drop_table_schemas.append("stg")
        drop_proc_names = [link_proc_name, sat_proc_name, job_proc_name]
        if self._validation_result.stg_schema == "proxy":
            drop_proc_names.append(stg_proc_name)
        drop_proc_code, drop_proc_name, drop_call_stmt = self.dialect_handler.make_drop_proc(
            entity_name=self._validation_result.entity_name,
            table_schemas=drop_table_schemas,
            procedures=drop_proc_names,
            header=self.header
        )
        procedures["drop"] = (drop_proc_code, drop_proc_name, drop_call_stmt)

        return procedures