#!/usr/bin/python3
#
# Copyright (c) 2017 Red Hat, Inc.
#
# This software is licensed to you under the GNU General Public License,
# version 2 (GPLv2). There is NO WARRANTY for this software, express or
# implied, including the implied warranties of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
# along with this software; if not, see
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
#
# Red Hat trademarks are not licensed under GPLv2. No permission is
# granted to use or replicate Red Hat trademarks that are incorporated
# in this software or its documentation.
#

""" A CLI utility for managing the Candlepin database. """

from __future__ import print_function
from optparse import OptionParser

import glob
import os
import sys

try:
    from commands import getstatusoutput
except ImportError:
    from subprocess import getstatusoutput

if os.path.exists('/usr/sbin/tomcat') and not os.path.exists('/usr/sbin/tomcat6'):
    TOMCAT = 'tomcat'
else:
    TOMCAT = 'tomcat6'

JBOSS_CLASSPATH = "/var/lib/jbossas/server/production/deploy/candlepin.war/WEB-INF/classes/"
TOMCAT_CLASSPATH = "/var/lib/" + TOMCAT + "/webapps/candlepin/WEB-INF/classes/"
JAR_PATH = "/var/lib/" + TOMCAT + "/webapps/candlepin/WEB-INF/lib/"

def run_command(command):
    (status, output) = getstatusoutput(command)
    if status > 0:
        error_out(command, status, output)
    return output

def error_out(command, status, output):
    sys.stderr.write("\n########## ERROR ############\n")
    sys.stderr.write("Error running command: %s\n" % command)
    sys.stderr.write("Status code: %s\n" % status)
    sys.stderr.write("Command output: %s\n" % output)
    raise Exception("Error running command")


class DbSetup(object):
    def __init__(self, username, password, db, community, verbose):
        self.username = username
        self.db = db
        self.password = password
        self.community = community
        self.verbose = verbose

    def create(self):
        raise NotImplementedError("Implemented by subclasses")

    def initialize_schema(self):
        print("Loading Candlepin schema")
        self._run_liquibase("db/changelog/changelog-create.xml")

    def drop(self):
        raise NotImplementedError("Implemented by subclasses")

    def validate(self):
        """ Validates an existing candlepin database. """
        print("Validating Candlepin database")
        self._run_liquibase("db/changelog/changelog-validate.xml")

    def update(self):
        """ Upgrades an existing candlepin database. """
        print("Migrating Candlepin database")
        self._run_liquibase("db/changelog/changelog-update.xml")

    def _run_liquibase(self, changelog_path):
        # Figure out what to append to the classpath for liquibase:
        classpath = ":".join(glob.glob(JAR_PATH + "*postgresql*.jar"))

        if os.path.exists('build/classes'):
            classpath = "%s:%s" % (classpath, 'build/classes')

        if os.path.exists(TOMCAT_CLASSPATH):
            classpath = "%s:%s" % (classpath, TOMCAT_CLASSPATH)

        if os.path.exists(JBOSS_CLASSPATH):
            classpath = "%s:%s" % (classpath, JBOSS_CLASSPATH)

        liquibase_options = "--driver=%s --classpath=%s --changelog-file=%s --url=\"%s\" --username=$DBUSERNAME \
            --headless=true --hub-mode=OFF" % (
            self.driver_class,
            classpath,
            changelog_path,
            self.jdbc_url,
        )

        os.environ["DBUSERNAME"] = self.username
        os.environ["JAVA_HOME"] = "/usr/lib/jvm/jre-17-openjdk"

        if self.password:
            os.environ["DBPASSWORD"] = self.password
            liquibase_options += " --password=$DBPASSWORD"

        # Add in output level. By default, Liquibase defaults to "off"
        liquibase_options += " --log-level=%s" % ('debug' if self.verbose else 'severe')

        print(liquibase_options)

        output = run_command("/usr/share/candlepin/liquibase.sh %s update -Dcommunity=%s" % (liquibase_options, self.community))
        print(output)


class PostgresqlSetup(DbSetup):
    def __init__(self, host, port, username, password, db, community, verbose, ssl, sslfactory, sslcertpath):
        super(PostgresqlSetup, self).__init__(username, password, db, community, verbose)
        self.host = host
        self.port = port
        self.driver_class = "org.postgresql.Driver"

        # Adjust the jdbc URL for correct deployment:
        self.jdbc_url = "jdbc:postgresql:"
        if host is not None:
            self.jdbc_url = "%s//%s" % (self.jdbc_url, host)
            # Requires host:
            if port is not None:
                self.jdbc_url = "%s:%s" % (self.jdbc_url, port)
            # Append / for the database name:
            self.jdbc_url = "%s/" % (self.jdbc_url)
        self.jdbc_url = "%s%s" % (self.jdbc_url, db)

        # Appending SSL connection details
        if ssl is not None and ssl.lower() == "true":
            if sslfactory is None:
                if sslcertpath is None:
                    self.jdbc_url = "%s%s" % (self.jdbc_url, "?ssl=true&sslfactory=org.postgresql.ssl.NonValidatingFactory")
                    print("Default sslfactory=org.postgresql.ssl.NonValidatingFactory is used as you have not provided factory details.")
                elif sslcertpath is not None:
                    self.jdbc_url = "%s%s%s" % (self.jdbc_url, "?ssl=true&sslmode=verify-full&sslrootcert=", sslcertpath)

            elif sslfactory is not None:
                if sslcertpath is None:
                    self.jdbc_url = "%s%s%s" % (self.jdbc_url, "?ssl=true&sslfactory=", sslfactory)

        print("Configuring PostgreSQL with JDBC URL: %s" % self.jdbc_url)

        if password:
            os.environ["PGPASSWORD"] = password

    def create(self):
        print("Creating candlepin database 2")
        command = "createdb -U %s" % (self.username)
        if self.host:
            command = "%s -h %s" % (command, self.host)
            if self.port:
                command = "%s -p %s" % (command, self.port)
        command = "%s %s" % (command, self.db)
        (status, output) = getstatusoutput(command)

        if status > 0 and output.find("already exists") > 0:
            print("Candlepin database already exists, skipping...")
            return
        elif status > 0:
            error_out(command, status, output)

    def drop(self):
        print("Dropping candlepin database")
        command = "dropdb -U %s %s" % (self.username, self.db)
        (status, output) = getstatusoutput(command)
        if status > 0 and output.find('does not exist') > 0:
            return
        elif status > 0:
            error_out(command, status, output)


if __name__ == "__main__":
    parser = OptionParser()
    parser.add_option("--create",
            dest="create", action="store_true", default=False,
            help="create the Candlepin database; cannot be used with --update or --validate")

    parser.add_option("--schema-only",
            dest="schema_only", action="store_true", default=False,
            help="assumes database is already created by another tool and "
                 "applies schema to database; used with --create")

    parser.add_option("--validate",
            dest="validate", action="store_true", default=False,
            help="validate the current state of the Candlepin database; cannot be used with --create")

    parser.add_option("--update",
            dest="update", action="store_true", default=False,
            help="update the Candlepin database; cannot be used with --create")

    parser.add_option("--drop",
            dest="drop", action="store_true", default=False,
            help="drop the existing Candlepin database before creating")

    parser.add_option("-u", "--user",
            dest="dbuser", default="candlepin",
            help="database user to use")

    parser.add_option("-d", "--database",
            dest="db", default="candlepin",
            help="database name to use")

    parser.add_option("-p", "--password",
            dest="dbpassword",
            help="database password to use")

    parser.add_option("--dbhost",
            dest="dbhost",
            help="the database host to use (optional)")

    parser.add_option("--dbport",
            dest="dbport",
            help="the database port to use (optional)")

    parser.add_option("--community",
            action="store_true", default=False,
            dest="community",
            help="true if used in a community fashion")

    parser.add_option("--verbose",
            dest="verbose", action="store_true", default=False,
            help="enables verbose logging/output")

    parser.add_option("--ssl",
            dest="ssl",
            help="true if ssl connection needed (optional)")

    parser.add_option("--sslfactory",
            dest="sslfactory",
            help="ssl validation factory for ssl connection (optional); used with --ssl true")

    parser.add_option("--sslcertpath",
            dest="sslcertpath",
            help="ssl cert filepath for ssl connection (optional); used with --ssl true")

    (options, args) = parser.parse_args()

    if (not options.create and not options.update and not options.validate):
        print("ERROR: Please specify --create, --update or --validate.")
        sys.exit(1)

    if (options.create and options.validate):
        print("ERROR: --create cannot be used with --validate")
        sys.exit(1)

    if (options.create and options.update):
        print("ERROR: --create cannot be used with --update")
        sys.exit(1)

    if options.schema_only and not options.create:
        print("ERROR: --schema-only must only be specified with --create.")
        sys.exit(1)

    if options.drop and options.schema_only:
        print("ERROR: --drop can not be used with --schema-only")
        sys.exit(1)

    if options.ssl:
        if options.sslfactory and options.sslcertpath:
            print("ERROR: --ssl, --sslfactory, and --sslcertpath are not supported together.")
            sys.exit(1)
    elif options.sslfactory or options.sslcertpath:
        print("ERROR: --sslfactory or --sslcertpath must only be specified with --ssl.")
        sys.exit(1)

    dbsetup = PostgresqlSetup(options.dbhost, options.dbport, options.dbuser, options.dbpassword,
        options.db, options.community, options.verbose, options.ssl, options.sslfactory, options.sslcertpath)

    if options.create:
        if options.drop:
            dbsetup.drop()

        if not options.schema_only:
            dbsetup.create()

        dbsetup.initialize_schema()

    if options.validate:
        dbsetup.validate()

    if options.update:
        dbsetup.update()
