"""Link to Fact strategy implementations."""
from datetime import datetime
from typing import Tuple

from sqlalchemy import Column, MetaData, Table, UniqueConstraint

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

from .base import BaseValidator, SchemaGenerator, ValidationResult


class Link2FactValidator(BaseValidator):
    def __init__(self, template: str):
        super().__init__(template)
        self._on_validate_staging = self._validate_staging

    @staticmethod
    def _validate_staging(stg_info: StgInfo, sys_info: Dv2SystemInfo) -> None:
        # -----------------
        # hk
        # -----------------
        hk_count = len(stg_info.hk_keys)
        stg_full_name = f"stg.{stg_info.stg_name}"

        # so only exactly 2 or 3 columns allowed right now
        if hk_count < 2: # own and at least one foreign
            raise Exception(f"At least 2 hk columns expected in `{stg_full_name}` for the `link2fact` template")
        if hk_count > 3:
            raise Exception(f"{hk_count} hk columns in `{stg_full_name}` for the `link2fact` template?! Are you sure?")
        if stg_info.degenerate_field is None:
            raise Exception(f"Degenerate field is required for `{stg_full_name}` for the `link2fact` template")

        # hk_key = (key_name, key_type)
        hk_keys_copy = list(stg_info.hk_keys.keys()).copy()
        is_own_hk_found = False
        expected_own_hk_column_name = f"hk_{stg_info.stg_name}"

        for hk_name in stg_info.hk_keys.keys():
            if hk_name == expected_own_hk_column_name:
                hk_keys_copy.remove(hk_name)
                is_own_hk_found = True
            else:
                # check that `name` from `hk_[name]` is existing entity
                for en in sys_info.entities_list:
                    if hk_name == f"hk_{en.entity_name}":
                        hk_keys_copy.remove(hk_name)

        if not is_own_hk_found:
            raise Exception(f"Column `{expected_own_hk_column_name}` has not been found in `{stg_full_name}`")

        if len(hk_keys_copy) > 0:
            raise Exception(f"There are no entities related to `{", ".join(hk_keys_copy)}` columns in the `{stg_full_name}`")

        # -----------------
        # BKs
        # -----------------
        # it should be a warning, not an error
        if len(stg_info.bk_keys) > 0:
            raise Exception("You dont need bk columns for the `link2fact` template")


class Link2FactSchemaGenerator(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)
        dg_key = self._validation_result.degenerate_field
        link_table.append_column(Column(dg_key[0], dg_key[1], nullable=False))
        uks.append(dg_key[0])
        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)
        # for transactional links
        dg_key = self._validation_result.degenerate_field
        if dg_key is not None:
            sat_table.append_column(Column(dg_key[0], dg_key[1], nullable=False))


        # 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 (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"]
        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 + [self._validation_result.degenerate_field],
            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
