#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# Copyright (C) 2008-2016 Glencoe Software, Inc. All Rights Reserved.
# Use is subject to license terms supplied in LICENSE.txt
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
   Plugin for our managing the OMERO database.

   Plugin read by omero.cli.Cli during initialization. The method(s)
   defined here will be added to the Cli class for later use.
"""
from __future__ import division

from future.utils import bytes_to_native_str
from future.utils import isbytes
from past.utils import old_div
from omero.cli import BaseControl
from omero.cli import CLI

from argparse import FileType, SUPPRESS

from omero.install.windows_warning import windows_warning, WINDOWS_WARNING

from omero_ext.path import path

import omero.java
import platform
import sys
import time

if sys.version_info >= (3, 0, 0):
    # Keep str behavior on Python 2
    from builtins import str


HELP = """Database tools for creating scripts, setting passwords, etc."""


if platform.system() == 'Windows':
    HELP += ("\n\n%s" % WINDOWS_WARNING)


class DatabaseControl(BaseControl):

    def _configure(self, parser):
        sub = parser.sub()

        script = sub.add_parser(
            "script", help="Generates a DB creation script")
        script.set_defaults(func=self.script)
        try:
            # Python 3
            ft = FileType(mode="w", encoding='utf-8')
        except TypeError:
            ft = FileType(mode="w")
        script.add_argument(
            "-f", "--file", type=ft,
            help="Optional file to save to. Use '-' for stdout.")

        script.add_argument("posversion", nargs="?", help=SUPPRESS)
        script.add_argument("pospatch", nargs="?", help=SUPPRESS)
        script.add_argument("pospassword", nargs="?", help=SUPPRESS)

        script.add_argument("--version", help=SUPPRESS)
        script.add_argument("--patch", help=SUPPRESS)
        script.add_argument("--password", help="OMERO root password")

        pw = sub.add_parser(
            "password",
            help="Prints SQL command for updating your root password")
        pw.set_defaults(func=self.password)
        pw_spec = pw.add_mutually_exclusive_group()
        pw_spec.add_argument("password", nargs="?")
        pw_spec.add_argument("--empty", action="store_true",
                             help=("Remove the password, "
                                   "allowing any for login when guest."))
        pw.add_argument("--user-id",
                        help="User ID to salt into the password. "
                        "Defaults to '0', i.e. 'root'",
                        default="0")

        for x in (pw, script):
            x.add_argument(
                "--no-salt", action="store_true",
                help="Disable the salting of passwords")

    def _lookup(self, key, defaults, args):
        """
        Get a value from a flag arg, positional arg, or default properties
        """
        propname = "omero.db." + key
        vdef = defaults.properties.getProperty(propname)
        varg = getattr(args, key)
        vpos = getattr(args, 'pos' + key)
        if varg:
            if vpos:
                self.ctx.die(
                    1, "ERROR: Flag and positional argument given for %s" % key
                    )
            v = varg
        elif vpos:
            v = vpos
        elif vdef:
            v = vdef
        else:
            self.ctx.die(1, "No value found for %s" % propname)
        self.ctx.err("Using %s for %s" % (v, key))
        return v

    def _has_user_id(self, args):
        return args and "user_id" in args and args.user_id is not None

    def _get_password_hash(self, args, root_pass=None, old_prompt=False):

        prompt = " for OMERO "
        if self._has_user_id(args) and not old_prompt:
            prompt += "user %s" % args.user_id
        else:
            prompt += "root user"
        root_pass = self._ask_for_password(prompt, root_pass)

        jars = str(self.ctx.dir / "lib" / "server") + "/*"
        cmd = ["ome.security.auth.PasswordUtil", root_pass]
        if not args.no_salt and self._has_user_id(args):
            cmd.append(args.user_id)
        p = omero.java.popen(["-cp", jars] + cmd)
        rc = p.wait()
        if rc != 0:
            out, err = p.communicate()
            self.ctx.die(rc, "PasswordUtil failed: %s" % err.decode(
                errors='replace'))
        value = p.communicate()[0]
        if not value or len(value) == 0:
            self.ctx.die(100, "Encoded password is empty")
        return value.strip().decode()

    def _copy(self, input_path, output, func, cfg=None):
            try:
                input = open(str(input_path), encoding='utf-8')
            except TypeError:
                input = open(str(input_path))
            try:
                for s in input:
                        try:
                            if cfg:
                                output.write(func(s) % cfg)
                            else:
                                output.write(func(s))
                        except Exception as e:
                            self.ctx.dbg(str(e))
                            self.ctx.die(
                                154, "Failed to map line: %s\nError: %s"
                                % (s, e))
            finally:
                input.close()

    def _make_replace(self, root_pass, db_vers, db_patch):
        def fix(str_in):
            if isbytes(str_in):
                str_in = bytes_to_native_str(str_in)
            return str_in
        def replace_method(str_in):
            str_out = str_in.replace("@ROOTPASS@", fix(root_pass))
            str_out = str_out.replace("@DBVERSION@", fix(db_vers))
            str_out = str_out.replace("@DBPATCH@", fix(db_patch))
            return str_out
        return replace_method

    def _db_profile(self):
        from omero.install.config_parser import PropertyParser
        property_lines = self.ctx.get_config_property_lines(self.dir)
        for property in PropertyParser().parse_lines(property_lines):
            if property.key == 'omero.db.profile':
                return property.val
        raise KeyError('Configuration key not set: omero.db.profile')

    def _sql_directory(self, db_vers, db_patch):
        """
        See #2689
        """
        dbprofile = self._db_profile()
        sql_directory = self.ctx.dir / "sql" / dbprofile / \
            ("%s__%s" % (db_vers, db_patch))
        if not sql_directory.exists():
            self.ctx.die(2, "Invalid Database version/patch: %s does not"
                         " exist" % sql_directory)
        return sql_directory

    def _create(self, sql_directory, db_vers, db_patch, password_hash, args,
                location=None):
        sql_directory = self._sql_directory(db_vers, db_patch)
        if not sql_directory.exists():
            self.ctx.die(2, "Invalid Database version/patch: %s does not"
                         " exist" % sql_directory)

        if args and args.file:
            output = args.file
            script = "<filename here>"
        else:
            script = "%s__%s.sql" % (db_vers, db_patch)
            location = old_div(path.getcwd(), script)
            try:
                output = open(location, 'w', encoding='utf-8')
            except TypeError:
                output = open(location, 'w')
            self.ctx.out("Saving to " + location)

        try:
            dbprofile = self._db_profile()
            header = old_div(sql_directory, ("%s-header.sql" % dbprofile))
            footer = old_div(sql_directory, ("%s-footer.sql" % dbprofile))
            if header.exists():
                # 73 multiple DB support. OMERO 4.3+
                cfg = {
                    "TIME": time.ctime(time.time()),
                    "DIR": sql_directory,
                    "SCRIPT": script}
                self._copy(header, output, str, cfg)
                self._copy(old_div(sql_directory,"schema.sql"), output, str)
                self._copy(old_div(sql_directory,"views.sql"), output, str)
                self._copy(
                    footer, output,
                    self._make_replace(password_hash, db_vers, db_patch), cfg)
            else:
                # OMERO 4.2.x and before
                output.write("""
--
-- GENERATED %s from %s
--
-- This file was created by the `omero db script` command
-- and contains an MD5 version of your OMERO root users's password.
-- You should think about deleting it as soon as possible.
--
-- To create your database:
--
--     createdb omero
--     psql omero < %s
--

BEGIN;
                """ % (time.ctime(time.time()), sql_directory, script))
                self._copy(old_div(sql_directory,"schema.sql"), output, str)
                self._copy(
                    old_div(sql_directory,"data.sql"), output,
                    self._make_replace(password_hash, db_vers, db_patch))
                self._copy(old_div(sql_directory,"views.sql"), output, str)
                output.write("COMMIT;\n")

        finally:
            output.flush()
            if output != sys.stdout:
                output.close()

    @windows_warning
    def password(self, args):
        root_pass = None
        user_id = 0
        old_prompt = True
        if self._has_user_id(args):
            user_id = args.user_id
            if user_id != '0':  # For non-root, use new_prompt
                old_prompt = False
        try:
            root_pass = args.password
        except Exception as e:
            self.ctx.dbg("While getting arguments:" + str(e))
        if args.empty:
            password_hash = ""
        else:
            password_hash = self._get_password_hash(args, root_pass,
                                                    old_prompt)
        self.ctx.out("UPDATE password SET hash = '%s' "
                     "WHERE experimenter_id = %s;""" %
                     (password_hash, user_id))

    @windows_warning
    def loaddefaults(self):
        try:
            data2 = self.ctx.initData({})
            output = self.ctx.readDefaults()
            self.ctx.parsePropertyFile(data2, output)
        except Exception as e:
            self.ctx.dbg(str(e))
            data2 = None
        return data2

    @windows_warning
    def script(self, args):
        if args.posversion is not None:
            self.ctx.err("WARNING: Positional arguments are deprecated")
        if platform.system() == 'Windows':
            self.ctx.out("\n%s\n" % WINDOWS_WARNING)

        defaults = self.loaddefaults()
        db_vers = self._lookup("version", defaults, args)
        db_patch = self._lookup("patch", defaults, args)
        root_pass = args.password
        if root_pass:
            if args.pospassword:
                self.ctx.die(
                    1, "ERROR: Flag and positional argument given for password"
                    )
        else:
            root_pass = args.pospassword
        if root_pass:
            self.ctx.err("Using password from commandline")

        args.user_id = "0"
        sql = self._sql_directory(db_vers, db_patch)
        pwhash = self._get_password_hash(args, root_pass, True)
        self._create(sql, db_vers, db_patch, pwhash, args)

try:
    register("db", DatabaseControl, HELP)
except NameError:
    if __name__ == "__main__":
        cli = CLI()
        cli.register("db", DatabaseControl, HELP)
        cli.invoke(sys.argv[1:])
