import time

from fabric.api import cd, env, lcd, local, put, run, settings
from fabric.contrib.files import exists
from fabric.contrib.project import upload_project

import os
import shutil
import sys

# stop and start apache decorator
def apache(f):
    def deco(self, *args, **kw):
        self.stop_apache()
        d = f(self, *args, **kw)
        self.start_apache()
        return d
    return deco

class Fabric(object):
    """
    Initialise an edina fabric helper.

    project - project name
    target - target directory (relative to $HOME)
    source_path - the path to the source code (relative to source root)
    apache_dir - full path to an apache installation (if None given assume
                $target_dir/www)
    """
    def __init__(self, project, target='local', source_path=None, apache_dir=None):
        self.project = project
        self.host = env.host

        self.source_root = local('pwd', capture=True).strip()
        if source_path:
            self.source_dir = os.sep.join((self.source_root, source_path))
        else:
            self.source_dir = self.source_root

        self.virtual_env = os.sep.join((os.environ['HOME'], target))

        if self.host:
            # note: use echo $HOME instead of '~'
            # as tilde doesn't work with remote exists
            self.target_dir = os.sep.join((run('echo $HOME'), target))
        else:
            self.target_dir = self.virtual_env

        self.proj_dir = os.sep.join((self.target_dir, project))

        if apache_dir:
            self.apache_dir = apache_dir
        else:
            self.apache_dir =  os.sep.join((self.target_dir, 'www'))

    def install(self, app=None, python=None):
        """
        Install a project from scratch, for DEV, BETA or LIVE

        app - the application to be run (currently only single app supported
              but multiple should be supported). If None the app has the same
              name as the project.
        python - the Python interpreter to use (e.g /usr/bin/python2.6)
        """

        if not app:
            app = self.project

        if self.host:
            ret_val = self._install_remote(app, python)
        else:
            # no host defined, install locally
            ret_val = self._install_local(app, python)

        return ret_val

    @apache
    def deploy(self, app=None):
        """
        Deploy an application to BETA. This simpy rsync the application to
        machine.

        app - the application to be run (currently only single app supported
              but multiple should be supported). If None the app has the same
              name as the project.
        """
        if not app:
            app = self.project

        local('rsync -av --exclude *~ %s %s@%s:%s ' % (os.sep.join((self.source_dir, app)),
                                                       env.user,
                                                       self.host,
                                                       self.proj_dir))

    @apache
    def release(self, app=None,
                skip_code_check=False,
                skip_unit_tests=False,
                do_django_tests=False):
        """
        Release new version of a single application.

        app - the application to be run
        skip_code_check - skip code checking with pep8? If code check fails,
                          release is postponed.
        skip_unit_tests- skip unit testing? If code tests fail, release is postponed.
        do_django_tests - do django unit tests? If false standard python unit
                          tests will be run (assuming skip_unit_tests is False)
        """
        if not app:
            app = self.project

        import_str = 'import %s' % app

        try:
            # get version of app
            if self.source_dir not in sys.path:
                sys.path.append(self.source_dir)

            exec import_str
            version = eval('%s.get_version()' % app)
        except ImportError as e:
            print ("\n*** Error: A function named get_version() must be defined in "
                   "%s%c__init__.py" % (app, os.sep))
            return False

        # check git for version tag
        tags = local('git tag -l', capture=True)
        version_str = 'v%s' % version
        if tags:
            tags = tags.split('\n')
            if version_str in tags:
                print 'Version %s already exists' % version
                return False

        # check code style
        if not skip_code_check:
            self.code_check(app)

        # run unit tests
        if not skip_unit_tests:
            self.run_tests(app, do_django=do_django_tests)

        app_dir = os.sep.join((self.proj_dir, app))
        if exists(app_dir):
            org_dir = '%s-%s' % (os.sep.join((self.proj_dir, app)),  str(time.time()).split('.')[0])
            run('mv %s %s' % (app_dir, org_dir))

        # upload_project requires that you cd into the target directory
        print os.sep.join((self.source_dir, app)), self.proj_dir
        with cd(self.proj_dir):
            upload_project(os.sep.join((self.source_dir, app)), self.proj_dir)

        # create tag
        msg = "%s release %s" % (app, version_str)
        local('git tag -a %s -m "%s"' % (version_str, msg))

        return True

    def restore(self, app=None, tag=None):
        """
        Restore a previously tagged version

        app - the django application
        tag - the tag to restore
        """

        if tag:
            # get current branch
            branches = local('git branch', capture=True)
            for branch in branches.split('\n'):
                if branch[0] == '*':
                    org_branch = branch[2:]
                    break

            # checkout tag
            tags = local('git checkout %s' % tag)

            # move existing deployment
            app_dir = os.sep.join((self.proj_dir, app))
            if exists(app_dir):
                org_dir = '%s-%s' % (os.sep.join((self.proj_dir, app)),  str(time.time()).split('.')[0])
                run('mv %s %s' % (app_dir, org_dir))

            # deploy app
            self.deploy(app)

            # restore original branch
            local('git checkout %s' % org_branch)
        else:
            tags = local('git tag -l', capture=True)
            if tags:
                print 'available:'
                print tags
                print '\n*** Need to specify a tag'
            else:
                print '*** No git tags found.'

    def run_tests(self, app=None, do_django=True):
        """
        Run tests locally

        app - the application to run tests on. If None all are run.
        do_django - do django unit tests?
        """
        if not app:
            app = self.project

        # if app has a manage.py file run django tests (in virtual environment),
        # otherwise run simple unit tests

        if do_django and os.path.exists(
            os.sep.join((self.source_dir, app, 'manage.py'))):

            if os.path.exists(self.virtual_env):
                proj_dir = os.sep.join((self.virtual_env, self.project))
                app_dir = os.sep.join((proj_dir, app))
                local('. %s/bin/activate && python %s/manage.py test' %
                      (proj_dir, app_dir), capture=False)
            else:
                print("Django tests should be run in virtual environment, but"
                      " one doesn't exist (%s)" % self.proj_dir)
        else:
            local('cd %s && python -m unittest %s.tests' % (
                self.source_dir, app), capture=False)

    def code_check(self, app=None):
        """
        Run pep8 code checker

        app - the app to code check
        """
        if not app:
            app = self.project

        """Run code style checker"""
        source_dir = os.sep.join((self.source_dir, app))
        local('find %s -name "*.py" | xargs pep8 --ignore=E221' % source_dir,
              capture=False)

    def _install_local(self, app, python=None):
        self.proj_dir = os.path.expanduser(self.proj_dir)
        if os.path.exists(self.proj_dir):

            # check if they want to delete existing installation
            msg = 'Directory %s exists.\nDo you wish to delete it(y/n)? > '
            msg = msg % self.proj_dir
            answer = raw_input(msg).strip()

            if answer != 'y':
                print 'Choosing not continue. Nothing installed.'
                return False

            local('rm -rf %s' % self.proj_dir)

        with lcd(self.target_dir):
            # create virtual environment
            local(self._build_ve_command(python))

            # install dependencies
            req_file = os.sep.join((self.source_root,
                                    'etc',
                                    'requirements.txt'))
            if os.path.isfile(req_file):
                self.virtualenv('pip install -r %s' % req_file)
            else:
                print '*** WARN: No dependencies found at %s' % req_file

            with lcd(self.project):
                # create link to app
                local('ln -s %s' % os.sep.join((self.source_dir, app)))

        return True

    def _install_remote(self, app, python):

        if exists(self.proj_dir):
            print "There already exists an installation %s on %s" % (
                self.proj_dir,
                self.host)
            return False

        with cd(self.target_dir):
            run(self._build_ve_command(python))

            with cd(self.project):
                req_file = os.sep.join((self.source_root,
                                        'etc',
                                        'requirements.txt'))
                if os.path.isfile(req_file):
                    run('mkdir etc')
                    remote_etc_dir = os.sep.join((self.target_dir, self.project, 'etc'))
                    put(req_file, remote_etc_dir)
                    self.venvremote('pip install -r %s' %
                                    os.sep.join((remote_etc_dir,
                                                 'requirements.txt')))
                else:
                    print '*** WARN: No dependencies found at %s' % req_file

        return True

    def _build_ve_command(self, python):
        # build virtual environment command based on project and python flag
        cmd = None

        pversion = ''
        if python:
            pversion = '-p %s' % python
        cmd = 'virtualenv --no-site-packages %s %s' % (pversion, self.project)

        return cmd

    def virtualenv(self, command, capture=False):
        """
        Run command using virtual environment and the current runtime directory.

        command - the command to run
        capture - capture command output?
        """
        return local('. %s/bin/activate && %s' % (self.proj_dir, command), capture=capture)

    def venvremote(self, command):
        """
        Run command using virtual environment.

        command - the command to run
        """
        return run('. %s/bin/activate && %s' % (self.proj_dir, command))

    def stop_apache(self):
        """
        Stop apache.
        """
        if 'init.d' in self.apache_dir:
            # debian/ubuntu style install
            cmd = self.apache_dir
        else:
            # source install
            cmd = '%s/bin/apachectl' % self.apache_dir

        return run('%s stop' % self._get_apache_path())

    def start_apache(self):
        """
        Start apache.
        """
        return run('%s start' % self._get_apache_path())

    def _get_apache_path(self):
        if 'init.d' in self.apache_dir:
            # debian/ubuntu style install
            cmd = 'sudo %s' % self.apache_dir
        else:
            # source install
            cmd = '%s/bin/apachectl' % self.apache_dir

        return cmd
