import os
import sys
import logging
import shutil

from baboon.baboon.monitor import Monitor
from baboon.baboon.transport import WatchTransport
from baboon.baboon.initializor import MetadirController
from baboon.baboon.transport import RegisterTransport, AdminTransport
from baboon.baboon.fmt import cinput, confirm_cinput, cwarn, csuccess, cerr

from baboon.baboon.config import check_user, check_server, check_project
from baboon.baboon.config import check_config, config, dump, SCMS
from baboon.common.logger import logger
from baboon.common.utils import exec_cmd
from baboon.common.errors.baboon_exception import BaboonException
from baboon.common.errors.baboon_exception import CommandException

logger = logging.getLogger(__name__)


def command(fn):
    def wrapped():
        try:
            return fn()
        except (BaboonException, CommandException) as err:
            cerr(err)
        except KeyboardInterrupt as err:
            print "Bye !"

    return wrapped


def start():
    """ Starts baboon client !
    """

    # Ensure the validity of the configuration file.
    check_config(add_mandatory_server_fields=['streamer', 'max_stanza_size'])

    metadirs = []
    monitor = None
    transport = None

    try:
        transport = _start_transport()
        monitor = _start_monitor()
        metadirs = _start_metadirs(monitor.handler.exclude)

        # Wait until the transport is disconnected before exiting Baboon.
        _wait_disconnect(transport)
    except BaboonException as err:
        logger.error(err)
    except KeyboardInterrupt:
        pass
    finally:
        _start_close(monitor, transport, metadirs)
        logger.info("Bye !")


@command
def register():
    """ Ask mandatory information and register the new user.
    """

    transport = None
    try:
        username = _get_username()
        passwd = _get_passwd()

        print("\nRegistration in progress...")

        # RegisterTransport uses the config attributes to register.
        config['user'] = {
            'jid': username,
            'passwd': passwd
        }

        # Registration...
        transport = RegisterTransport(callback=_on_register_finished)
        transport.open(block=True)
    finally:
        # Disconnect the transport if necessary.
        if transport and transport.connected.is_set():
            transport.close()
            transport.disconnected.wait(10)


@command
def projects():
    """ Lists all users in a project.
    """

    check_config()

    project = config['parser'].get('project')
    subs_by_project = []

    with AdminTransport(logger_enabled=False) as transport:
        # Get all subscriptions
        subscriptions = _projects_specific(transport, project) if project \
            else _projects_all(transport)

        # Display the subscriptions in a good format.
        _projects_print_users(subscriptions)


@command
def create():
    """ Create a new project with the project argument name.
    """

    check_server()
    check_user()

    project = config['parser']['project']
    path = config['parser']['path']
    project_scm = _check_scm(path)

    _check_project(project)
    config['projects'][project] = {
        'path': path,
        'scm': project_scm,
        'enable': 1
    }

    print("Creation in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.create_project(project)
        if not _on_action_finished(ret_status, msg):
            return

    dump()

    # Do an init() if the git-url can be guessed.
    git_url = _get_git_url(path)
    if git_url:
        config['parser']['git-url'] = git_url
        init()


@command
def delete():
    """ Delete the project with the project argument name.
    """

    check_config()

    project = config['parser']['project']
    print("Deletion in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.delete_project(project)
        _on_action_finished(ret_status, msg)

    project_path = _get_project_path(project)
    _delete_metadir(project, project_path)
    del config['projects'][project]
    dump()


@command
def join():
    """ Join the project with the project argument name.
    """

    check_server()
    check_user()

    project = config['parser']['project']
    path = config['parser']['path']
    project_scm = _check_scm(path)

    _check_project(project)
    config['projects'][project] = {
        'path': path,
        'scm': project_scm,
        'enable': 1
    }

    print("Join in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.join_project(project)
        if not _on_action_finished(ret_status, msg):
            return

    dump()


@command
def unjoin():
    """ Unjoin the project with the project argument name.
    """

    check_config()

    project = config['parser']['project']
    print("Unjoin in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.unjoin_project(project)
        _on_action_finished(ret_status, msg)

    project_path = _get_project_path(project)
    _delete_metadir(project, project_path)
    del config['projects'][project]
    dump()


@command
def accept():
    """ Accept the username to the project.
    """

    check_config()

    project = config['parser']['project']
    username = _get_username()

    print("Acceptation in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.accept_pending(project, username)
        _on_action_finished(ret_status, msg)


@command
def reject():
    """ Reject the username to the project.
    """

    check_config()

    project = config['parser']['project']
    username = _get_username()

    print("Rejection in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.reject(project, username)
        _on_action_finished(ret_status, msg)


@command
def kick():
    """ Kick the username to the project.
    """

    check_config()

    project = config['parser']['project']
    username = _get_username()

    print("Kick in progress...")
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.kick(project, username)
        _on_action_finished(ret_status, msg)


@command
def init():
    """ Initialialize a new project.
    """

    check_config()

    project = config['parser']['project']
    project_path = _get_project_path(project)
    url = config['parser']['git-url']

    print("Initialize the project %s..." % project)
    with AdminTransport(logger_enabled=False) as transport:
        ret_status, msg = transport.first_git_init(project, url)

        metadir_controller = MetadirController(project, project_path)
        metadir_controller.init_index()
        metadir_controller.create_baboon_index()
        metadir_controller.index.close()

        if not _on_action_finished(ret_status, msg):
            _delete_metadir(project, project_path)


def _start_transport():
    """ Builds and returns a new connected WatchTransport.
    """

    transport = WatchTransport()
    transport.open()
    transport.connected.wait()

    return transport


def _start_monitor():
    """ Builds and returns a new watched Monitor.
    """

    monitor = Monitor()
    monitor.watch()

    return monitor


def _start_metadirs(exclude=None):
    """ Builds and returns all metadirs. exclude is the exclude_method
    optionally needed by the MetadirController constructor.
    """

    metadirs = []
    for project, project_attrs in config['projects'].iteritems():
        # For each project, verify if the .baboon metadir is valid and
        # take some decisions about needed actions on the repository.
        metadir = MetadirController(project, project_attrs['path'], exclude)
        metadirs.append(metadir)
        metadir.go()

    return metadirs


def _start_close(monitor, transport, metadirs):
    """ Clears the monitor, transport and list of metadir before finishing the
    start command.
    """

    logger.info("Closing baboon...")

    # Close each metadir shelve index.
    for metadir in metadirs:
        metadir.index.close()

    # Close the transport and the monitor. If one of them is not
    # started, the close() method has no effect.
    if monitor:
        monitor.close()
    if transport:
        transport.close()
        transport.disconnected.wait(10)


def _wait_disconnect(transport, timeout=5):
    """ Polls the state of the transport's connection each `timeout` seconds.
    Exits when the transport is disconnected.
    """

    while not transport.disconnected.is_set():
        transport.disconnected.wait(timeout)


def _get_username():
    """ Returns the username by getting it from the config or asking it from
    stdin.
    """

    username = config['parser'].get('username')
    if not username:
        validations = [('^\w+$', 'Username can only contains alphanumeric and '
                        'underscore characters')]
        username = cinput('Username: ', validations=validations)

    # Transform the username to a JID.
    username += '@%s' % config['parser']['hostname']

    return username


def _get_passwd():
    """ Returns the password by getting it from stdin.
    """

    validations = [('^\w{6,}$', 'The password must be at least 6 characters '
                    'long.')]
    return confirm_cinput('Password: ', validations=validations, secret=True,
                          possible_err='The password must match !')


def _projects_specific(transport, project):
    """ Lists all users in a specific project. The transport must be connected.
    """

    project_users = transport.get_project_users(project) or []
    return [(project, project_users)]


def _projects_all(transport):
    """ Lists all users in a all projects. The transport must be connected.
    """

    subscriptions = []
    for project in config['projects']:
        subscriptions += _projects_specific(transport, project)

    return subscriptions


def _projects_print_users(subs_by_project):
    """ Prints the subs_by_project list of tuples.
    """

    for project, subs in subs_by_project:
        print("[%s]" % project)
        for sub in subs:
            print(" %s" % sub['jid'])


def _check_project(project_name):
    """ Checks if the project is not already defined in the configuration file.
    If so, raise a CommandException.
    """

    project = config['projects'].get(project_name)
    if project:
        if project.get('enable') == '0':
            raise CommandException(409, "The project is already defined in "
                                   "your configuration file, but it's "
                                   "disabled.")

        raise CommandException(409, "The project is already defined in your "
                               "configuration file.")


def _check_scm(path):
    """ Checks if the SCM in the path directory is supported. If not, raise a
    CommandException.
    """

    # Ensure the path exists.
    if not os.path.exists(path):
        raise CommandException(404, "The project's path does not exist on "
                               "your system.")

    # Ensure the path is a directory.
    if not os.path.isdir(path):
        raise CommandException(500, "The project's path is not a directory.")

    # Ensure the scm in the path directory exists and is supported.
    scm = _get_scm(path)
    if not scm:
        raise CommandException(500, "The project isn't managed by a supported "
                               "source code manager.")

    return scm


def _get_project_path(project_name):
    """ Returns the project path of the project_name. Raised a CommandException
    if cannot be retrieved.
    """

    try:
        project_path = config['projects'][project_name]['path']
        return project_path
    except KeyError:
        raise CommandException(404, "The project path cannot be found in your "
                               "configuration file.")


def _get_git_url(path):
    """ Try to auto-detect the git origin url in the path dir. If found, return
    it.
    """

    ret_code, output, _ = exec_cmd('git config --get remote.origin.url', path)
    return output if ret_code == 0 else None


def _delete_metadir(project_name, project_path):
    """ Delete the metadir on the project_path. Raised a CommandException on
    error.
    """

    try:
        MetadirController(project_name, project_path).delete()
    except EnvironmentError:
        raise CommandException(500, "Cannot delete the metadir directory.")


def _on_register_finished(ret_status, msg, fatal=False):
    """ Callback for the registration.
    """

    _on_action_finished(ret_status, msg, fatal=fatal)
    # Dump the configuration file if there's no error.
    if ret_status == 200:
        dump()


def _on_action_finished(ret_status, msg, fatal=False):
    if ret_status >= 200 and ret_status < 300:
        csuccess(msg)
        return True
    else:
        # Print the error message.
        cerr(msg)
        return False


def _get_scm(path):
    """ Explores the path of the directory and returns the SCM used (one None).
    """

    for scm in SCMS:
        if os.path.isdir(os.path.join(path, '.%s' % scm)):
            return scm
