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.notifier import Notifier
 
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'])
 
    # If notification is configured, start the notifier.
    try:
        notif_cmd = config['notification']['cmd']
        Notifier(notif_cmd)
    except:
        pass
 
    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