from collections import namedtuple

from django.db.backends.base.introspection import (
    BaseDatabaseIntrospection, FieldInfo as BaseFieldInfo, TableInfo,
)
from django.utils.regex_helper import _lazy_re_compile

FieldInfo = namedtuple('FieldInfo', BaseFieldInfo._fields + ('pk',))
collation_re = _lazy_re_compile(r"^VARCHAR\(\d+\) COLLATE '([\w+\-]+)'$")
field_size_re = _lazy_re_compile(r'^[A-Z]+\((\d+)\)')
precision_and_scale_re = _lazy_re_compile(r'^NUMBER\((\d+),(\d+)\)$')


def get_collation(name):
    """
    Return the collation from a "VARCHAR(11) COLLATE 'collation'" type name.
    """
    m = collation_re.search(name)
    return m[1] if m else None


def get_data_type(name):
    """Extract the data type name from an ABC(...) type name."""
    return name.split('(', 1)[0]


def get_field_size(name):
    """Extract the size number from a VARCHAR(11) type name."""
    m = field_size_re.search(name)
    return int(m[1]) if m else None


def get_precision_and_scale(name):
    """
    Return the precision (first number) and scale (second number) from a
    NUMBER(38,0) type name.
    """
    m = precision_and_scale_re.search(name)
    return (int(m[1]), int(m[2])) if m else (None, None)


class DatabaseIntrospection(BaseDatabaseIntrospection):
    # Maps Snowflake data types returned by `DESCRIBE TABLE` to Django Fields.
    data_types_reverse = {
        'BINARY': 'BinaryField',
        'BOOLEAN': 'BooleanField',
        'DATE': 'DateField',
        'FLOAT': 'FloatField',
        'NUMBER': 'BigIntegerField',
        'TIME': 'TimeField',
        'TIMESTAMP_LTZ': 'DateTimeField',
        'VARCHAR': 'CharField',
    }

    def get_constraints(self, cursor, table_name):
        constraints = {}
        # Foreign keys
        cursor.execute(f'SHOW IMPORTED KEYS IN TABLE "{table_name}"')
        for row in cursor.fetchall():
            constraints[row[12]] = {
                'columns': [row[8]],
                'primary_key': False,
                'unique': False,
                'foreign_key': (row[3], row[4]),
                'check': False,
                'index': False,
            }
        # Primary keys
        cursor.execute(f'SHOW PRIMARY KEYS IN TABLE "{table_name}"')
        for row in cursor.fetchall():
            constraints[row[6]] = {
                'columns': [row[4]],
                'primary_key': True,
                'unique': False,
                'foreign_key': None,
                'check': False,
                'index': False,
            }
        # Unique constraints
        cursor.execute(f'SHOW UNIQUE KEYS IN TABLE "{table_name}"')
        # The columns of multi-column unique indexes are ordered by row[5].
        # Map {constraint_name: [(row[5], column_name), ...] so the columns can
        # be sorted for each constraint.
        unique_column_orders = {}
        for row in cursor.fetchall():
            column_name = row[4]
            constraint_name = row[6]
            if constraint_name in constraints:
                # If the constraint name is already present, this is a
                # multi-column unique constraint.
                constraints[constraint_name]['columns'].append(column_name)
                unique_column_orders[constraint_name].append((row[5], column_name))
            else:
                constraints[constraint_name] = {
                    'columns': [column_name],
                    'primary_key': False,
                    'unique': True,
                    'foreign_key': None,
                    'check': False,
                    'index': False,
                }
                unique_column_orders[constraint_name] = [(row[5], column_name)]
        # Order the columns of multi-column unique indexes.
        for constraint_name, orders in unique_column_orders.items():
            constraints[constraint_name]['columns'] = [col for _, col in sorted(orders)]
        return constraints

    def get_primary_key_column(self, cursor, table_name):
        pks = [field.name for field in self.get_table_description(cursor, table_name) if field.pk]
        return pks[0] if pks else None

    def get_relations(self, cursor, table_name):
        """
        Return a dictionary of {field_name: (field_name_other_table, other_table)}
        representing all foreign keys in the given table.
        """
        cursor.execute(f'SHOW IMPORTED KEYS IN TABLE "{table_name}"')
        return {row[8]: (row[4], row[3]) for row in cursor.fetchall()}

    def get_field_type(self, data_type, description):
        field_type = super().get_field_type(data_type, description)
        # 16777216 is the default size if max_length isn't specified.
        if data_type == 'VARCHAR' and description.internal_size == 16777216:
            return 'TextField'
        # Handle NUMBER if it's something besides BigAutoField.
        if data_type == 'NUMBER':
            if description.scale != 0:
                return 'DecimalField'
            elif description.precision == 5:
                field_type = 'SmallIntegerField'
            elif description.precision == 10:
                field_type = 'IntegerField'
        # Handle AutoField and variants.
        if description.default and 'IDENTITY' in description.default:
            if field_type == 'IntegerField':
                return 'AutoField'
            elif field_type == 'BigIntegerField':
                return 'BigAutoField'
            elif field_type == 'SmallIntegerField':
                return 'SmallAutoField'
        return field_type

    def get_table_description(self, cursor, table_name):
        cursor.execute('DESCRIBE TABLE %s' % self.connection.ops.quote_name(table_name))
        table_info = cursor.fetchall()
        return [
            FieldInfo(
                # name, type_code, display_size, internal_size,
                name, get_data_type(data_type), None, get_field_size(data_type),
                # precision, scale, null_ok, default,
                *get_precision_and_scale(data_type), null == 'Y', default,
                # collation, pk,
                get_collation(data_type), pk == 'Y',
            )
            for (
                name, data_type, kind, null, default, pk, unique_key, check,
                expression, comment, policy_name,
            ) in table_info
        ]

    def get_table_list(self, cursor):
        cursor.execute('SHOW TERSE TABLES')
        tables = [TableInfo(row[1], 't') for row in cursor.fetchall()]
        cursor.execute('SHOW TERSE VIEWS')
        views = [TableInfo(row[1], 'v') for row in cursor.fetchall()]
        return tables + views
