#
# OMNIVORE CONFIDENTIAL
# __________________
#
#  [2013] - [2021] Omnivore Technologies
#  All Rights Reserved.
#
# NOTICE:  All information contained herein is, and remains
# the property of Omnivore Technologies and its suppliers,
# if any.  The intellectual and technical concepts contained
# herein are proprietary to Omnivore Technologies
# and its suppliers and may be covered by U.S. and Foreign Patents,
# patents in process, and are protected by trade secret or copyright law.
# Dissemination of this information or reproduction of this material
# is strictly forbidden unless prior written permission is obtained
# from Omnivore Technologies.
#

import argparse
import datetime
import logging
import sys
import time

from sqlalchemy import create_engine, text

logger = None
LOG_LEVEL = logging.WARN

CACHE_ID_COL = '_cache_id'

MASTER_LEVEL_TABLES = [
    'daily_cache_logs',
    'stores',
    'scheduled_tasks',
    'ticket_close_candidates',
]

STORE_LEVEL_TABLES = [
    'clock_entries',
    'clock_entry_breaks',
    'clock_entry_segments',
    'discounts',
    'employees',
    'held_payments',
    'held_ticket_discounts',
    'held_ticket_item_discounts',
    'held_ticket_item_modifier_closures',
    'held_ticket_item_modifiers',
    'held_ticket_items',
    'held_ticket_service_charges',
    'held_ticket_totals',
    'held_tickets',
    'jobs',
    'menu_categories',
    'menu_category_menu_item',
    'menu_category_menu_modifier',
    'menu_items',
    'modifier_group_modifier',
    'modifier_groups',
    'modifiers',
    'option_sets',
    'order_types',
    'pay_rate_levels',
    'pay_rates',
    'payments',
    'price_levels',
    'revenue_centers',
    'service_charges',
    'tables',
    'tender_types',
    'ticket_discounts',
    'ticket_ids',
    'ticket_item_discounts',
    'ticket_item_modifier_closures',
    'ticket_item_modifiers',
    'ticket_items',
    'ticket_service_charges',
    'ticket_totals',
    'tickets',
    'user_data',
]


def make_parser():
    result = argparse.ArgumentParser()

    result.add_argument(
        '-c', '--conn-string',
        help='Connection string for the database to migrate',
        required=True,
        default=None,
    )
    result.add_argument(
        'action',
        choices=['upgrade', 'downgrade'],
    )

    log_level_group = result.add_mutually_exclusive_group()
    log_level_group.add_argument("--verbose", "-v",
                                 action="store_const", dest="log_level",
                                 const=logging.INFO, default=None)
    log_level_group.add_argument("--debug", "-vv",
                                 action="store_const", dest="log_level",
                                 const=logging.DEBUG)
    log_level_group.add_argument("--quiet", "-q",
                                 action="store_const", dest="log_level",
                                 const=logging.CRITICAL)

    return result


def _print(message, *args):
    if LOG_LEVEL < logging.ERROR:
        message = '[{}] {}'.format(
            datetime.datetime.now().isoformat(' '),
            message,
        )
        if args:
            message %= args
        print message


def _migrate_cache_ids(engine, table_name, cache_ids, is_master_level_table, new_cache_id_strategy):
    if not cache_ids:
        logger.warn("Skipping `%s` because the table is empty", table_name)

    update_sql = '''
        UPDATE `{table_name}`
        SET `{CACHE_ID_COL}` = :new_cache_id
        WHERE `{CACHE_ID_COL}` = :orig_cache_id
    '''.format(
        table_name=table_name,
        CACHE_ID_COL=CACHE_ID_COL,
    ).strip()
    cache_conversions = {
        cache_id: new_cache_id_strategy(cache_id, is_master_level_table)
        for cache_id in cache_ids
    }

    connection = engine.connect()
    transaction = None
    rowcount = 0
    new_cache_ids = set()
    try:
        transaction = connection.begin()
        connection.execute(text('SET FOREIGN_KEY_CHECKS = 0'))
        logger.debug('SQL: %s', update_sql)
        for orig_cache_id, new_cache_id in cache_conversions.iteritems():
            if new_cache_id is None:
                if orig_cache_id is None:
                    logger.error(
                        "Found rows from '%s' with NULL cache id. The DB may have corrupted cache data. "
                        "Skipping migration for these rows...",
                        table_name,
                    )
                else:
                    logger.warn(
                        "Skipping rows from `%s` with _cache_id = %r "
                        "because they have already been migrated.",
                        table_name,
                        orig_cache_id,
                    )
                new_cache_ids.add(orig_cache_id)
                continue
            if new_cache_id in new_cache_ids:
                logger.warn(
                    "Skipping rows from '%s' with _cache_id = %r "
                    "to avoid conflict with existing rows with _cache_id = %r",
                    table_name,
                    orig_cache_id,
                    new_cache_id,
                )
                continue
            params = dict(orig_cache_id=orig_cache_id, new_cache_id=new_cache_id)
            logger.debug("Params: %r", params)
            response = connection.execute(text(update_sql), **params)
            logger.info(
                "Updated %d rows from `%s` with _cache_id = %r. New value: %r",
                response.rowcount,
                table_name,
                orig_cache_id,
                new_cache_id,
            )
            new_cache_ids.add(new_cache_id)
            rowcount += response.rowcount
        transaction.commit()
        logger.info("Migrated %d rows from table `%s`", rowcount, table_name)
    except:
        if transaction is not None:
            transaction.rollback()
        raise
    finally:
        connection.execute(text('SET FOREIGN_KEY_CHECKS = 1'))
        connection.close()


def _migrate_table(engine, table_name, new_cache_id_strategy, is_master_level_table=False):
    table_start = time.time()
    select_sql = "SELECT `{CACHE_ID_COL}` FROM `{table_name}` GROUP BY `{CACHE_ID_COL}`".format(
        CACHE_ID_COL=CACHE_ID_COL,
        table_name=table_name,
    )
    logger.debug(select_sql)
    start = time.time()
    cache_ids = [row[0] for row in engine.execute(text(select_sql))]
    logger.debug("Received %d cache ids in %.4g seconds", len(cache_ids), time.time() - start)
    _migrate_cache_ids(engine, table_name, cache_ids, is_master_level_table, new_cache_id_strategy)
    _print("'%s' migration completed in %.4g seconds", table_name, time.time() - table_start)


def upgrade(connection_string):
    def new_cache_id(cache_id, is_master_level_table):
        if cache_id is None:
            return None
        cache_id_parts = cache_id.split(':', 3)
        if len(cache_id_parts) < 2:
            return None

        result = (
            cache_id_parts[1]
            if is_master_level_table else
            cache_id_parts[2]
        )
        return result

    engine = create_engine(connection_string)
    for table_name in MASTER_LEVEL_TABLES:
        _migrate_table(
            engine,
            table_name,
            is_master_level_table=True,
            new_cache_id_strategy=new_cache_id,
        )
    for table_name in STORE_LEVEL_TABLES:
        _migrate_table(
            engine,
            table_name,
            is_master_level_table=False,
            new_cache_id_strategy=new_cache_id,
        )


def downgrade(connection_string):
    stores_masters = {}

    def new_cache_id(cache_id, is_master_level_table):
        if cache_id is None or ':' in cache_id:
            return None

        if is_master_level_table:
            result = 'v1-0:{}:'.format(cache_id)
        else:
            try:
                master_id = stores_masters[cache_id]
            except KeyError:
                logger.error("Could not find master for store '%s'. Skipping downgrade.", cache_id)
                raise
            result = 'v1-0:{}:{}'.format(master_id, cache_id)
        return result

    engine = create_engine(connection_string)
    stores_masters = dict(list(engine.execute(text("SELECT id, _cache_id FROM stores"))))

    for table_name in MASTER_LEVEL_TABLES:
        _migrate_table(
            engine,
            table_name,
            is_master_level_table=True,
            new_cache_id_strategy=new_cache_id,
        )
    for table_name in STORE_LEVEL_TABLES:
        _migrate_table(
            engine,
            table_name,
            is_master_level_table=False,
            new_cache_id_strategy=new_cache_id,
        )


def main():
    global logger, LOG_LEVEL

    args = make_parser().parse_args(sys.argv[1:])

    LOG_LEVEL = args.log_level or logging.WARN
    logging.basicConfig(level=LOG_LEVEL, format='[%(asctime)s] (%(levelname)s) %(name)s: %(message)s')
    logger = logging.getLogger(__name__)

    if args.action == 'upgrade':
        upgrade(args.conn_string)
    else:
        downgrade(args.conn_string)


if __name__ == '__main__':
    main()
