import hashlib
import typing

from pg_sql import SqlId, SqlNumber, SqlObject, SqlString, sql_list

from .formats.join import JoinTable
from .join_common import Structure, context_column, foreign_column, local_column
from .join_key import KeyResolver
from .sql import SqlQuery, SqlTableExpr, table_fields, update_excluded
from .string import indent


def create_queue(
    id: str,
    table_id: str,
    structure: Structure,
    resolver: KeyResolver,
    tables: typing.Dict[str, JoinTable],
    context: typing.List[str],
):
    table = tables[table_id]
    dep = table.join
    foreign_table = tables[dep]

    if table.lock_id is not None:
        lock_id = table.lock_id
    else:
        digest = hashlib.md5(f"{id}__{table_id}".encode("utf-8")).digest()
        lock_id = int.from_bytes(digest[0:2], "big", signed=True)
    lock_base = lock_id * (2 ** 48)

    queue_table = structure.queue_table(table_id)

    column_names = [column.name for column in table.key] if table.key else ["_"]

    local_columns = [local_column(column) for column in column_names]
    foreign_columns = [foreign_column(column) for column in table.join_key]
    context_columns = [context_column(setting) for setting in context]

    columns = (
        [
            f"{SqlObject(SqlId('l'), SqlId(column))} AS {local_column(column)}"
            for column in column_names
        ]
        + [
            f"{SqlObject(SqlId('f'), SqlId(column))} AS {foreign_column(column)}"
            for column in table.join_key
        ]
        + [f"NULL::text AS {context_column(setting)}" for setting in context]
        + ["NULL::bigint AS seq", "NULL::bigint AS lock", "NULL::bigint AS count"]
    )

    yield f"""
CREATE TABLE {queue_table}
AS SELECT {sql_list(columns)}
FROM
  {table.sql} AS l
  CROSS JOIN {foreign_table.sql} AS f
WITH NO DATA
    """.strip()

    yield f"""
ALTER TABLE {queue_table}
  ADD PRIMARY KEY ({sql_list(local_columns + context_columns)}),
  ALTER count SET NOT NULL,
  ALTER count SET DEFAULT 0,
  ALTER lock ADD GENERATED BY DEFAULT AS IDENTITY,
  ALTER lock SET NOT NULL,
  ALTER seq ADD GENERATED BY DEFAULT AS IDENTITY,
  ALTER seq SET NOT NULL
    """.strip()

    yield f"""
COMMENT ON TABLE {queue_table} IS {SqlString(f"Asynchronous processing of changes to {table.sql}")}
    """.strip()

    for column in column_names:
        yield f"""
COMMENT ON COLUMN {queue_table}.{local_column(column)} IS {SqlString(f"{table.sql} key: {SqlId(column)}")}
"""

    for column in table.join_key:
        yield f"""
COMMENT ON COLUMN {queue_table}.{foreign_column(column)} IS {SqlString(f"{foreign_table.sql} iterator: {SqlId(column)}")}
"""

    yield f"""
COMMENT ON COLUMN {queue_table}.seq IS 'Order to process'
    """.strip()

    yield f"""
COMMENT ON COLUMN {queue_table}.lock IS 'Lock ID (add to base value {lock_base})'
    """.strip()

    yield f"""
COMMENT ON COLUMN {queue_table}.count IS 'Count of records processed'
    """.strip()

    yield f"""
CREATE INDEX ON {queue_table} (seq)
    """.strip()

    foreign_key_table = SqlObject(SqlId("_foreign_key"))

    item = SqlId("_item")
    new_item = SqlId("_new_item")

    new_fields = ["''" for _ in context] + ["0", "0", "_item.count + count(*) OVER ()"]
    get_item = f"""
SELECT
  {table_fields(item, local_columns)},
  {table_fields(SqlId("k"), [SqlId(column) for column in table.join_key])},
  {sql_list(new_fields)}
INTO _new_item
FROM {SqlObject(foreign_key_table)} AS k
ORDER BY {table_fields(SqlId("k"), table.join_key)} DESC
    """.strip()

    if table.join_on is not None:
        join = f"""
JOIN (VALUES ({table_fields(item, local_columns)})) AS {SqlId(table_id)} ({sql_list(SqlId(col.name) for col in table.key)})
ON {table.join_on}
        """.strip()
    else:
        join = ""

    key1_query = f"""
SELECT {SqlId(dep)}.*
FROM {foreign_table.sql} AS {SqlId(dep)}
{join}
ORDER BY {sql_list(SqlObject(SqlId(dep), SqlId(name)) for name in table.join_key)}
LIMIT max_records
    """.strip()
    gather1 = resolver.sql(
        foreign_key_table,
        exprs=[SqlTableExpr(foreign_key_table, key1_query)],
        last_expr=get_item,
    )

    key2_query = f"""
SELECT {SqlId(dep)}.*
FROM {foreign_table.sql} AS {SqlId(dep)}
{join}
WHERE ({table_fields(item, foreign_columns)}) < ({table_fields(SqlId(dep), (SqlId(column) for column in table.join_key))})
ORDER BY {sql_list(SqlObject(SqlId(dep), SqlId(name)) for name in table.join_key)}
LIMIT max_records
    """.strip()
    gather2 = resolver.sql(
        foreign_key_table,
        exprs=[SqlTableExpr(foreign_key_table, key2_query)],
        last_expr=get_item,
    )

    context_vars = "\n".join(
        f"{SqlId(f'_context_{setting}')} text := current_setting({SqlString(setting)}, true);"
        for setting in context
    )
    set_context = "\n".join(
        f"PERFORM set_config({SqlString(setting)}, _item.{context_column(setting)}, true);"
        for setting in context
    )
    unset_context = "\n".join(
        f"PERFORM set_config({SqlString(setting)}, {SqlId(f'_context_{setting}')}, true);"
        for setting in context
    )

    process_function = structure.queue_process_function(table_id)
    yield f"""
CREATE FUNCTION {process_function} (max_records bigint) RETURNS bool
LANGUAGE plpgsql AS $$
  DECLARE
    _item {queue_table};
    _new_item {queue_table};
    {context_vars}
  BEGIN
    -- find item
    SELECT (q.*) INTO _item
    FROM {queue_table} AS q
    WHERE pg_try_advisory_xact_lock({lock_base} + q.lock)
    ORDER BY q.seq
    LIMIT 1;

    IF _item IS NULL THEN
      -- if no item found, exit
      RETURN false;
    END IF;

{indent(set_context, 2)}

    IF ({table_fields(item, (foreign_column(column) for column in table.join_key))}) IS NULL THEN
      -- if there is no iterator, start at the beginning
{indent(gather1, 3)}
    ELSE
      -- if there is an iterator, start at the iterator
{indent(gather2, 3)}
    END IF;

    IF _new_item IS NULL THEN
      -- if the iterator was at the end, remove the queue item
      DELETE FROM {queue_table} AS q
      WHERE
        ({table_fields(SqlId("q"), local_columns + context_columns)}, q.seq)
          = ({table_fields(item, local_columns + context_columns)}, _item.seq);
    ELSE
      -- update the queue item with the new iterator
      UPDATE {queue_table} AS q
      SET
        {sql_list(f'{column} = (_new_item).{column}' for column in foreign_columns)},
        count = _new_item.count,
        seq = nextval(pg_get_serial_sequence({SqlString(str(queue_table))}, 'seq'))
      WHERE
          ({table_fields(SqlId("q"), local_columns)}, q.seq)
          = ({table_fields(item, local_columns)}, _item.seq);
    END IF;

{indent(unset_context, 2)}

    -- notify listeners that the queue has been updated
    NOTIFY {SqlId(str(queue_table))};

    RETURN true;
  END;
$$
""".strip()

    yield f"""
COMMENT ON FUNCTION {process_function} IS {SqlString(f"Refresh for {queue_table}")}
    """.strip()


def enqueue_sql(
    id: str,
    context: typing.List[str],
    table: JoinTable,
    structure: Structure,
    key_query: str,
    exprs: typing.List[SqlTableExpr],
    last_expr: typing.Optional[str],
):
    queue_table = structure.queue_table(id)

    column_names = [column.name for column in table.key] if table.key else ["_"]
    local_columns = [local_column(column) for column in column_names]
    context_columns = [context_column(setting) for setting in context]

    if table.key:
        order = (
            f"ORDER BY {sql_list(SqlNumber(i + 1) for i, _ in enumerate(table.key))}"
        )
    else:
        order = ""

    if context:
        settings = sql_list(
            f"coalesce(current_setting({SqlString(f'context.{setting}')}, true), '')"
            for setting in context
        )
        key_query = f"""
SELECT *, {settings}
FROM (
{indent(key_query, 1)}
) AS t
        """.strip()

    insert = f"""
INSERT INTO {queue_table} ({sql_list(local_columns + context_columns)})
{key_query}
{order}
ON CONFLICT ({sql_list(local_columns + context_columns)}) DO UPDATE
  SET {update_excluded(foreign_column(column) for column in table.join_key)},
    count = excluded.count,
    seq = excluded.seq
    """.strip()
    query = SqlQuery(insert, expressions=exprs)
    if last_expr is not None:
        query.append(SqlId("_other"), last_expr)

    return f"""
{query};

NOTIFY {SqlId(str(queue_table))};
    """.strip()
