import logging

from django.db import connection

logger = logging.getLogger(__name__)


def chunkit(large, sizes):
    """Yields successive n-sized chunks from large list."""
    for i in range(0, len(large), sizes):
        k = i + sizes
        yield large[i:k]


def get_concrete_fields(model):
    """
    Returns concrete fields (fields that have a column in the database)
    It includes foreign key fields.
    """
    # usable attributes on fields
    # https://docs.djangoproject.com/en/dev/ref/models/fields/#attributes-for-fields
    return [field for field in model._meta.get_fields() if field.concrete]


def _get_as_holders(column_names, first_row):
    quoted_columns = [connection.ops.quote_name(name) for name in column_names]
    holders = ", ".join([f"%s AS {name}" for name in quoted_columns])
    query = "\nSELECT {holders} UNION ALL".format(holders=holders)
    params = list(first_row)
    return query, params


def _get_value_holders(column_names, params):
    holders = ", ".join(["%s"] * len(column_names))
    query = "\nSELECT {holders} UNION ALL".format(holders=holders)
    return query, params


def get_tmp_table(column_names, values):
    query, params = _get_as_holders(column_names, values[0])
    for row in values[1:]:
        _query, _params = _get_value_holders(column_names, row)
        query += _query
        params += _params
    # Strip out last UNION ALL
    last_union_all = " UNION ALL"
    query = query[: -len(last_union_all)]
    return query, params


# ==========
# UTILITY FUNCTIONS TO FIND NEW INSTANCES
# ==========
def get_compare_fields(model, ignore_fields=None):
    ignore_fields = ignore_fields or ["id", "pk", "create_date", "update_date"]
    # we want the actual column names in the model
    # in case of foreign keys (eg company_code),
    # the field name is "company_code" while "attname" is company_code_id
    # the dependent fields (eg companyother in companycode) don't have attname
    compare_fields = {
        field.attname
        for field in get_concrete_fields(model)
        if hasattr(field, "attname") and field.attname not in ignore_fields
    }
    return compare_fields


def _debug_mismatch(model_name, model, mismatching_instances, compare_fields):
    if model._meta.model_name == model_name.lower() and mismatching_instances:
        i = mismatching_instances[0]
        for f in compare_fields:
            print(f, getattr(i, f))


def _compare_field(field):
    col = connection.ops.quote_name(field.attname)
    left = f"cte.{col}"
    right = f"model_table.{col}"
    if field.null:
        return f"(({left} IS NULL AND {right} IS NULL) OR ({left} = {right}))"
    return f"{left} = {right}"


def _get_match_status(model, compare_fields, instances):
    # create a temporary table using UNION ALL TECHNIQUE
    column_values = [
        [getattr(instance, field) for field in compare_fields] for instance in instances
    ]
    temp_table, temp_table_params = get_tmp_table(compare_fields, column_values)

    # check if the rows match in model
    fields = [
        field
        for field in get_concrete_fields(model)
        if hasattr(field, "attname") and field.attname in compare_fields
    ]
    column_compare_sql = " AND ".join(_compare_field(field) for field in fields)

    model_table = connection.ops.quote_name(model._meta.db_table)
    SQL = f"""
    SELECT
        EXISTS(
            SELECT *
            FROM {model_table} AS model_table
            WHERE {column_compare_sql}
        ) AS is_found
    FROM ({temp_table}) as cte;
    """

    with connection.cursor() as cursor:
        cursor.execute(SQL, temp_table_params)
        results = cursor.fetchall()

    # convert array of tuple to simple array
    results = [result[0] for result in results]
    return results


def find_existing(instances, compare_fields=None, ignore_fields=None, chunk_size=4000):
    """
    Segregates instances into exact same and mismatching instances
    - Mismatching means instances which have different values or are new
    """
    if compare_fields and ignore_fields:
        raise ValueError("Provide either compare_fields or ignore_fields, not both")

    if not instances:
        return [], []

    # get model from instance in django
    model = instances[0]._meta.model

    # fields to ignore while comparing
    compare_fields = compare_fields or get_compare_fields(model, ignore_fields)

    # get statuses in the chunks of 1000
    statuses = []
    for chunk in chunkit(instances, chunk_size):
        statuses.extend(_get_match_status(model, compare_fields, chunk))

    matching_instances = []
    mismatching_instances = []
    for i, instance in enumerate(instances):
        if statuses[i]:
            matching_instances.append(instance)
        else:
            mismatching_instances.append(instance)
    return matching_instances, mismatching_instances
