#!/usr/bin/python3

from __future__ import annotations

from typing import Dict

import pymysql
import pymysql.cursors
import argparse

from pymysql import Connection
from pymysql.cursors import Cursor

desc = """
overview:
    The script updates the value of a subscriber's 'sound_set' with that of
    the 'contract_sound_set' property.

    If any conflicts arise, the user will be prompted with the option to skip
    the sound set update for the current subscriber, or substitute the
    existing value of the sound_set property for that of the value of
    'contract_sound_set'.
"""

db_host = "localhost"
db_port = 3306
database = "provisioning"

subscribers: Dict[int, Subscriber] = {}

args = None
is_verbose = False


class QueryResult:
    """Result of the query."""

    def __init__(self, _id, attribute, value):
        self.id = _id
        self.data = attribute
        self.value = value

    def attributes(self) -> tuple:
        return tuple(self.__dict__.values())


class Subscriber:
    """
    Subscriber object.

    The subscriber object stores all the sound set configurations for any given
    subscriber.
    """

    def __init__(self, _id: int):
        """Initialize the subscriber object."""
        self.id = _id
        self.sound_set = ""
        self.contract_sound_set = ""

    def __str__(self) -> str:
        """String representation of the subscriber object."""
        return (
            f"Sound set configuration for subscriber with ID {self.id} \n"
            f"-> sound_set={self.sound_set} \n"
            f"-> contract_sound_set={self.contract_sound_set}"
        )

    def set_attribute(self, attribute, value) -> None:
        """
        Set a subscriber object attribute.

        :param attribute: target attribute of the subscriber object.
        :param value: value of the attribute.
        """
        setattr(self, attribute, value)

    def generate_commit_queries(self) -> tuple[str, str]:
        """
        Generate query for the subscriber object, with
        update-first-then-insert logic.

        :return list containing the update and insert queries.
        """

        target_table = "provisioning.voip_usr_preferences"
        id_column = "subscriber_id"

        update_query = f"""
            UPDATE {target_table} u
            JOIN provisioning.voip_preferences p ON u.attribute_id = p.id
            SET u.value = CASE
              WHEN p.attribute = 'sound_set'
                THEN '{self.sound_set}'
              WHEN p.attribute = 'contract_sound_set'
                THEN '{self.contract_sound_set}'
            END
            WHERE u.{id_column} = {self.id}
            AND p.attribute IN ('sound_set', 'contract_sound_set');
        """

        insert_query = f"""
            INSERT INTO {target_table} ({id_column}, attribute_id, value)
            SELECT {self.id}, p.id,
              CASE
                WHEN p.attribute = 'sound_set'
                  THEN '{self.sound_set}'
                WHEN p.attribute = 'contract_sound_set'
                  THEN '{self.contract_sound_set}'
              END AS value
            FROM provisioning.voip_preferences p
            WHERE p.attribute IN ('sound_set', 'contract_sound_set')
              AND NOT EXISTS (
                SELECT 1
                FROM {target_table} u
                WHERE u.{id_column} = {self.id}
                  AND u.attribute_id = p.id
            );
        """
        return (update_query, insert_query)


def logit(str, level, end) -> None:
    """
    General logger.

    Format messages and prints to stdout.

    :param str: message to log
    :param level: logging severity level
    :param end: end character
    :return:
    """
    print("%s-> %s" % (" " * 4 * level, str), end=end)


def log(str, level=0, end="\n") -> None:
    """Print a log message to the terminal."""
    logit(str, level, end)


def log_verbose(str, level=0, end="\n") -> None:
    """Print a log message to the terminal if verbose mode is active."""
    global is_verbose
    if is_verbose is True:
        logit(str, level, end)


def error(str, level=0, end="\n") -> None:
    """Print an error message to the terminal."""
    logit("Error: " + str, level, end)


def connect_db() -> Connection[Cursor] | int:
    """
    Connect to the database.

    :return: the connection object or 0 if there is an error.
    """

    try:
        db_connection = pymysql.connect(
            host=db_host,
            port=db_port,
            read_default_file="/etc/mysql/sipwise_extra.cnf",
            database=database,
        )
        log_verbose("Connected to %s:%s as 'sipwise'" % (db_host, db_port))
        db_connection.autocommit(False)
        return db_connection
    except Exception as e:
        error("Could not connect to %s:%s as 'sipwise': %s"
              % (db_host, db_port, e))
        return 0


def fetch_subscriber_sound_sets_query() -> str:
    """
    Fetch list of the subscribers that has at least one of the preferences set.

    :return: a list of subscribers that have at least one of the preferences
    set.
    """
    return """
        SELECT s.id, s.username, s.domain_id, p.attribute, u.value
        FROM provisioning.voip_subscribers s,
             provisioning.voip_usr_preferences u,
             provisioning.voip_preferences p
        WHERE p.id = u.attribute_id
          AND s.id = u.subscriber_id
          AND p.attribute
        IN ('sound_set', 'contract_sound_set')
        ORDER BY s.username, p.attribute;
    """


def initialize_subscriber_objects(results: tuple) -> None:
    """
    Create all subscriber objects from query results.

    :param results: tuple of the query results.
    """
    ids = set(map(lambda result: result[0], results))
    for _id in ids:
        subscribers[_id] = Subscriber(_id)


def extract_query_result_variables(result: tuple) -> QueryResult:
    """
    Extract query result variables.

    :param result: tuple of the query results.
    """
    return QueryResult(_id=result[0], attribute=result[3], value=result[4])


def get_sound_set_data() -> None:
    """Query database for sound set data."""
    db_connection = connect_db()
    if type(db_connection) is Connection:
        with db_connection.cursor() as cursor:
            cursor.execute(fetch_subscriber_sound_sets_query())
            results = cursor.fetchall()
            initialize_subscriber_objects(results)
            for row in results:
                result = extract_query_result_variables(row)
                _id, attribute, value = result.attributes()
                subscriber = subscribers[_id]
                subscriber.set_attribute(attribute, value)


def get_conflict_resolution_mode(
    is_mode_skip: bool, is_mode_substitute: bool
) -> int:
    """Sets conflict resolution mode.

    Outputs the conflict resolution mode in case of conflicting sound set
    values.

    :param is_mode_skip: True to set conflict resolution mode to 'skip'.
    :param is_mode_substitute: True to set conflict resolution mode to
      'replace'.
    """
    if is_mode_skip:
        log_verbose("'skip-all' mode has been enabled")
        return 1
    if is_mode_substitute:
        log_verbose("'substitute-all' mode has been enabled")
        return 2

    return 0


def update_sound_set_preferences(conflict_handler: int) -> None:
    """
    Update the 'sound_set' preferences for all subscribers.

    :param conflict_handler: conflict handling mode
    """
    for _id, subscriber in subscribers.items():
        log_verbose(
            f"Inspecting sound set preferences for subscriber with "
            f"ID {subscriber.id}"
        )
        if not subscriber.contract_sound_set:
            log("No changes were made, 'contract_sound_set' property is empty")
            return
        else:
            if not subscriber.sound_set:
                log(
                    f"Updating subscriber 'sound_set' property value to "
                    f"{subscriber.contract_sound_set}"
                )
                subscriber.sound_set = subscriber.contract_sound_set
            else:
                action = "0"
                if conflict_handler > 0:
                    action = str(conflict_handler)
                else:
                    action = input(
                        f"Conflicting sound sets associated to subscriber "
                        f"with ID {_id} have been found!\n"
                        "In order to resolve this issue, "
                        "please choose one of the following options:\n"
                        "1. skip       >>> the value of the "
                        "'sound_set' property will be left as is\n"
                        "2. substitute >>> the value of the "
                        "'sound_set' property will be replaced with "
                        "the value of 'contract_sound_set'\n"
                        "Please, select option 1, 2: "
                    )
                match action:
                    case "1":
                        log_verbose("Skipping subscriber")
                    case "2":
                        log(
                            f"Substituting subscriber 'sound_set' property "
                            f"value from {subscriber.sound_set} to "
                            f"{subscriber.contract_sound_set}"
                        )
                        subscriber.sound_set = subscriber.contract_sound_set
                    case _:
                        log_verbose(
                            "Sorry, I don't know how to handle this action. "
                            "This subscriber will be skipped"
                        )
    return


def commit_changes() -> None:
    """Commit changes to the database."""
    log_verbose("Commiting changes for all subscribers")
    for subscriber in subscribers.values():
        [update_query, insert_query] = subscriber.generate_commit_queries()
        if update_query is not None and insert_query is not None:
            db_connection = connect_db()
            if type(db_connection) is Connection:
                with db_connection.cursor() as cursor:
                    cursor.execute(update_query)
                    cursor.execute(insert_query)
                    db_connection.commit()
                    db_connection.close()
    log_verbose("Successfully commited codec names for all subscribers")


def main() -> None:
    """Main function."""
    global args
    global is_verbose
    argparser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter, description=desc
    )
    conflict_resolution_group = argparser.add_mutually_exclusive_group()
    argparser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="enable verbose output",
    )
    conflict_resolution_group.add_argument(
        "-s",
        "--skip-all",
        action="store_true",
        help="skip all substitutions in case of conflicts",
    )
    conflict_resolution_group.add_argument(
        "-S",
        "--substitute-all",
        action="store_true",
        help=(
            "substitute all conflicting 'sound_set' properties with "
            "the value of the subscriber's 'contract_sound_set'"
        ),
    )
    args, macros = argparser.parse_known_args()
    is_verbose = args.verbose
    is_substitute_all = args.substitute_all
    is_skip_all = args.skip_all

    log_verbose("verbose mode has been enabled")
    conflict_handler = (
        get_conflict_resolution_mode(is_skip_all, is_substitute_all)
    )
    connect_db()
    log("fetching data...")

    get_sound_set_data()
    update_sound_set_preferences(conflict_handler)
    commit_changes()


if __name__ == "__main__":
    main()
