#!/usr/bin/env python2

import urwid
import inspect
import sys
import os
import traceback
import shutil
import time
import logging
import argparse

import ceph_ansible_copilot

from ceph_ansible_copilot.utils import (PluginMgr, restore_ansible_cfg,
                                        SSHConfig)

from ceph_ansible_copilot.ui import (UI_Welcome,
                                     UI_Environment,
                                     UI_Host_Definition,
                                     UI_Credentials,
                                     UI_Host_Validation,
                                     UI_Network,
                                     UI_Commit,
                                     UI_Deploy,
                                     UI_Finish,
                                     Breadcrumbs,
                                     ProgressOverlay)

from ceph_ansible_copilot.ui.palette import palette

CEPH_ANSIBLE_ROOT = '/usr/share/ceph-ansible'


def unknown_input(key):

    if key == 'esc':
        raise urwid.ExitMainLoop


class Settings(object):

    def __repr__(self):
        items = [(attr, getattr(self, attr))
                 for attr in self.__dict__ if not attr.startswith('_')]
        s = '\n'
        for attr, value in items:
            if isinstance(value, Settings):
                s += '{}:\n'.format(attr)
                sub_items = repr(value).split("\n")
                for i in sub_items[:-1]:
                    s += '- {}\n'.format(i)
            else:
                s += '{}: {}\n'.format(attr, value)
        return s


class Config(Settings):

    # TODO : read a config file to set up defaults
    # and merge with a saved config

    def __init__(self):
        self.defaults = Settings()
        self.defaults.osd_objectstore = 'filestore'
        self.defaults.sw_src = 'RH CDN'
        self.defaults.dmcrypt = 'standard'
        self.defaults.playbook = '/usr/share/ceph-ansible/site.yml'

        self.hosts = None


def get_ui_sections():
    sections = {}

    for name, obj in inspect.getmembers(sys.modules[__name__],
                                        inspect.isclass):
        if name.startswith('UI_'):
            ui_class = getattr(sys.modules[__name__], name)
            if "seq_no" in ui_class.__dict__:
                seq_no = ui_class.seq_no
            else:
                seq_no = ui_class.lineno()
            sections[seq_no] = (ui_class, ui_class.title)

    return sections


class App(object):
    """
    Main control logic object. It constructs the interface from the other
    classes
    """

    def __init__(self):

        pgm = '{} v{}'.format(
                         os.path.splitext(os.path.basename(__file__))[0],
                         ceph_ansible_copilot.__version__)

        banner = urwid.Columns([
            urwid.Text(("title", pgm),
                       align='left'),
            urwid.Text(("title", "[{}]".format(opts.mode)),
                       align='right')
        ])

        self.title = urwid.AttrMap(banner, "title")

        self.timestamp = None
        self.file_timestamp = None
        self.log = None
        self.pagenum = 0
        self.page = []      # list of UI pages/views
        self.pb_active = False
        self.pb_complete = 0
        self.pb = None
        self.debug = None           # used to check state during debugging

        self.plugin_mgr = None
        self.ssh = None

        self.msg = None
        self.msg_text = None
        self.left_pane = None
        self.right_pane = None
        self.top = None

        self.loop = None
        self.cfg = Config()
        self.opts = opts
        self.hosts = dict()

        if opts.playbook:
            self.playbook = opts.playbook
        else:
            self.playbook = self.cfg.defaults.playbook

        self.ansible_cfg = os.path.join(CEPH_ANSIBLE_ROOT, 'ansible.cfg')
        self.ansible_cfg_bkup = '{}_bak'.format(self.ansible_cfg)

    def refresh_ui(self, left=None, right=None):
        if not left:
            left = self.left_pane.breadcrumbs
        if not right:
            right = self.page[self.pagenum].render_page

        body = urwid.Columns([('fixed',
                               Breadcrumbs.breadcrumb_width,
                               left),
                              right])

        self.top = urwid.Frame(body,
                               header=self.title,
                               footer=self.msg)

    def next_page(self):
        if self.pagenum < len(self.page) - 1:
            self.pagenum += 1

        copilot.left_pane.update()
        self.msg_text = self.page[self.pagenum].hint
        self.show_message(self.msg_text)

        new_page = self.page[self.pagenum]
        new_page.refresh()

        copilot.refresh_ui(left=self.left_pane.breadcrumbs,
                           right=new_page.render_page)

        self.loop.widget = self.top

    def execute_plugins(self):

        self.cfg.hosts = self.hosts
        self.cfg.ceph_version = opts.ceph_version
        self.cfg.cluster_name = opts.cluster_name

        self.log.info("Configuration options supplied:")
        self.log.info(self.cfg)

        self.log.info("End of options")

        plugin_status = {
            "successful": 0,
            "failed": 0
        }

        num_plugins = len(self.plugin_mgr.plugins)
        if num_plugins > 0:
            self.log.info("Plugin execution starting..")

        srtd_names = sorted(self.plugin_mgr.plugins.keys())

        plugins = self.plugin_mgr.plugins

        for plugin_name in srtd_names:
            mod = plugins[plugin_name].module
            yml_file = mod.yml_file

            try:
                self.log.info("Plugin: {}".format(plugin_name))
                plugin_data = mod.plugin_main(self.cfg)

                plugins[plugin_name].executed = True

            except BaseException as error:
                # Use BaseException as a catch-all from the plugins
                plugin_status['failed'] += 1
                self.log.error("Plugin '{}' failed : "
                               "{}".format(plugin_name,
                                           sys.exc_info()[0]))
                self.log.debug(traceback.format_exc())
                break
            else:
                plugin_status['successful'] += 1
                if isinstance(plugin_data, tuple):
                    f_type, contents = plugin_data
                    if contents:
                        self.write_yml(yml_file, contents, f_type)
                        self.log.info("Plugin updated {}".format(yml_file))
                    else:
                        self.log.info("Plugin - no data written to "
                                      "{}".format(yml_file))
                elif not plugin_data:
                    self.log.info("backup and update handled by plugin")

        skipped = num_plugins - (plugin_status['successful'] +
                                 plugin_status['failed'])

        self.log.info("{} plugin(s) sucessful, "
                      "{} failed, "
                      "{} skipped".format(plugin_status['successful'],
                                          plugin_status['failed'],
                                          skipped))

        return plugin_status

    def write_yml(self, yml_file, contents, file_type='yml'):
        self.bkup_yml(yml_file)
        contents.insert(0, ' ')
        contents.insert(0, "# created by copilot - only overrides from"
                           " defaults shown {}".format(self.file_timestamp))
        if file_type == 'yml':
            contents.insert(0, '---')

        # unbuffered write
        with open(yml_file, 'w', 0) as f:
            f.write("\n".join(contents))

        # read it back
        with open(yml_file, 'r') as f:
            contents = f.readlines()

    def bkup_yml(self, yml_file):

        if os.path.exists(yml_file):
            yml_file_bkup = '{}_{}'.format(yml_file,
                                           self.timestamp)
            shutil.copy2(yml_file, yml_file_bkup)
            self.log.info("YML file {}, copied to {}".format(yml_file,
                                                             yml_file_bkup))
        else:
            self.log.warning("Existing file {}, not found".format(yml_file))

    def show_message(self, msg_text, immediate=False):
        self.msg_text = msg_text
        msg = msg_text.lower()
        if msg.startswith('error'):
            attr = 'error_message'
        elif msg.startswith('warning'):
            attr = 'warning message'
        else:
            attr = 'message'
        self.msg.set_attr_map({None: attr})
        self.msg.base_widget.set_text(self.msg_text)

        if immediate:
            self.loop.draw_screen()

    def progress_bar(self, complete=0):
        if not self.pb_active:
            # turn on a progress bar
            self.pb_active = True

            self.pb = ProgressOverlay(bottom_w=self.top, complete=complete)
            self.loop.widget = self.pb
            self.loop.draw_screen()
        else:
            # turn the progress bar off
            self.pb_active = False
            self.pb = None
            self.loop.widget = self.top
            self.loop.draw_screen()

    def progress_bar_update(self, stats):
        if self.pb_active:
            task_state = stats['task_state']
            done = sum([task_state[item] for item in task_state])
            self.loop.widget = self.pb.update(done)
            self.loop.draw_screen()

        else:
            return

    def setup(self):

        self.ssh = SSHConfig()
        if not self.ssh.configured:
            print("Unable to access/create ssh keys")
            sys.exit(4)

        self.file_timestamp = time.ctime()
        self.timestamp = int(time.time())
        self.log = setup_logging()
        self.log.info("{} (v{}) starting at "
                      "{}".format(os.path.basename(__file__),
                                  ceph_ansible_copilot.__version__,
                                  self.file_timestamp))

        self._setup_dirs()

        self.plugin_mgr = PluginMgr(logger=self.log)
        self.log.info("{} plugin(s) "
                      "loaded".format(len(self.plugin_mgr.plugins)))

        for plugin_name in self.plugin_mgr.plugins:
            mod = self.plugin_mgr.plugins[plugin_name].module
            self.log.info("- {}".format(mod.__file__[:-1]))     # *.pyc -> *.py

        self.init_UI()

        self.loop = urwid.MainLoop(copilot.top,
                                   palette,
                                   unhandled_input=unknown_input)

    def _setup_dirs(self):

        group_vars = '/etc/ansible/group_vars'
        ceph_ansible_vars = os.path.join(CEPH_ANSIBLE_ROOT, 'group_vars')
        if os.path.exists(group_vars):
            if os.path.realpath(group_vars) == ceph_ansible_vars:
                self.log.info("ceph-ansible group_vars is linked to ansible's"
                              " directory - no change needed")
            else:
                raise EnvironmentError("group_vars already exists, but doesn't"
                                       " point to ceph-ansible group_vars")
        else:
            # Doesn't exists, so create the symlink to ceph-ansible's dir
            if os.path.exists(ceph_ansible_vars):
                os.symlink(ceph_ansible_vars, group_vars)
                self.log.info("Created symlink to ceph-ansible group_vars")
            else:
                raise EnvironmentError("ceph-ansible group_vars not found. Is "
                                       "ceph-ansible installed?")

    def check_keys(self):
        keys_dir = os.path.join(os.path.expanduser('~'), 'ceph-ansible-keys')
        if os.path.exists(keys_dir):
            self.log.info("Ansible keys directory is ready, no action needed")
        else:
            os.mkdir(keys_dir)
            self.log.info("Ansible keys directory created")

    def init_UI(self):
        ui_elements = get_ui_sections()
        section_names = []
        for idx in sorted(ui_elements):
            (page_class, page_title) = ui_elements[idx]
            section_names.append(page_title)
            self.page.append(page_class(self))

        self.left_pane = Breadcrumbs(self, section_names)
        self.right_pane = self.page[self.pagenum]
        self.msg_text = self.page[self.pagenum].hint

        self.msg = urwid.AttrMap(
                     urwid.Text(self.msg_text), 'message')

        self.refresh_ui(left=self.left_pane,
                        right=self.right_pane.render_page)
        return

    def cleanup(self):

        # if we had a site_yml plugin run it again in delete mode
        # to remove the additional include_vars tasks
        if self.plugin_mgr.plugins['site_yml'].executed:
            self.log.info("running site_yml again to remove "
                          "the include_vars task for all.yml")
            mod = self.plugin_mgr.plugins['site_yml'].module
            mod.plugin_main(config=self.cfg, mode='delete')

        # if we have a _bak version of the ansible.cfg, restore it to it's
        # previous state
        restore_ansible_cfg()


def setup_logging():

    log_path = '/var/log/ceph-ansible-copilot.log'
    logger = logging.getLogger("copilot")
    logger.setLevel(logging.DEBUG)
    file_handler = logging.FileHandler(log_path, mode='w')

    file_format = logging.Formatter(
        '%(asctime)s [%(levelname)-8s] %(message)s')

    file_handler.setFormatter(file_format)
    file_handler.setLevel(logging.DEBUG)
    logger.addHandler(file_handler)

    return logger


def parse_cli_options():

    modes = ['dev', 'prod']                     # 1st entry is the default!
    copilot_version = ceph_ansible_copilot.__version__

    parser = argparse.ArgumentParser(description="ceph-ansible copilot")

    parser.add_argument("--mode", "-m", type=str, choices=modes,
                        default=modes[0],
                        help="type of cluster to be deployed "
                             "(default is {})".format(modes[0]))

    parser.add_argument("--cluster-name", "-n", type=str,
                        default='ceph',
                        help="cluster name override (default is 'ceph')")

    parser.add_argument("--playbook", "-P", type=str,
                        help="playbook to use for deployment ")

    parser.add_argument("--ceph-version", "-c", type=int,
                        default=12, choices=[10, 12],
                        help="ceph version to install")

    parser.add_argument('--version', action='version',
                        version='{} {}'.format(parser.prog,
                                               copilot_version))

    args = parser.parse_args()
    return args


if __name__ == "__main__":

    opts = parse_cli_options()

    # check that the cwd is /usr/share/ceph-ansible to pick up the correct
    # environment (cfg, plugins, actions etc)
    if os.getcwd() != CEPH_ANSIBLE_ROOT:
        print("-> copilot must be started from the {} "
              "directory".format(CEPH_ANSIBLE_ROOT))
        sys.exit(4)

    if opts.playbook:
        if not os.path.exists(opts.playbook):
            print("-> playbook file not found. Is it fully qualified?")
            sys.exit(4)

    copilot = App()
    copilot.setup()
    copilot.loop.run()
    copilot.cleanup()

    print("--- DEBUG STUFF ---")
    print("Config:")
    print(copilot.cfg)
    selected_hosts = [host_name for host_name in copilot.hosts
                      if copilot.hosts[host_name].selected is True]

    print("Selected hosts are: {}".format(selected_hosts))