import os
import re
from importlib import import_module
from types import ModuleType

import mock
import peewee as pw
from cached_property import cached_property

from peewee_migrate import LOGGER, MigrateHistory
from peewee_migrate.auto import diff_many, NEWLINE
from peewee_migrate.compat import string_types, exec_in
from peewee_migrate.migrator import Migrator


CLEAN_RE = re.compile(r'\s+$', re.M)
MIGRATE_DIR = os.path.join(os.getcwd(), 'migrations')
VOID = lambda m, d: None # noqa
with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'template.txt')) as t:
    MIGRATE_TEMPLATE = t.read()


class BaseRouter(object):

    """Abstract base class for router."""

    def __init__(self, database, migrate_table='migratehistory', logger=LOGGER):
        self.database = database
        self.migrate_table = migrate_table
        self.logger = logger
        if not isinstance(self.database, (pw.Database, pw.Proxy)):
            raise RuntimeError('Invalid database: %s' % database)

    @cached_property
    def model(self):
        """Ensure that migrations has prepared to run."""
        # Initialize MigrationHistory model
        MigrateHistory._meta.database = self.database
        MigrateHistory._meta.db_table = self.migrate_table
        MigrateHistory.create_table(True)
        return MigrateHistory

    @property
    def todo(self):
        raise NotImplementedError

    def create(self, name='auto', auto=False):
        """Create a migration."""
        migrate = rollback = ''
        if auto:
            if isinstance(auto, string_types):
                try:
                    auto = import_module(auto)
                except ImportError:
                    return self.logger.error("Can't import models module: %s", auto)

            if isinstance(auto, ModuleType):
                auto = list(filter(
                    lambda m: isinstance(m, type) and issubclass(m, pw.Model),
                    (getattr(auto, model) for model in dir(auto))))  # noqa

            for migration in self.diff:
                self.run_one(migration, self.migrator)

            models1 = auto
            models2 = list(self.migrator.orm.values())

            migrate = diff_many(models1, models2, migrator=self.migrator)
            if not migrate:
                return self.logger.warn('No changes found.')

            migrate = NEWLINE + NEWLINE.join('\n\n'.join(migrate).split('\n'))
            migrate = CLEAN_RE.sub('\n', migrate)

            rollback = diff_many(models2, models1, migrator=self.migrator, reverse=True)
            rollback = NEWLINE + NEWLINE.join('\n\n'.join(rollback).split('\n'))
            rollback = CLEAN_RE.sub('\n', rollback)

        self.logger.info('Creating migration "%s"', name)
        path = self._create(name, migrate, rollback)
        self.logger.info('Migration created %s', path)
        return path

    def _create(self, name, migrate='', rollback=''):
        raise NotImplementedError

    def read(self, name):
        raise NotImplementedError

    @property
    def done(self):
        """Scan migrations in database."""
        return [mm.name for mm in self.model.select()]

    @property
    def diff(self):
        """Calculate difference between fs and db."""
        done = set(self.done)
        return [name for name in self.todo if name not in done]

    @cached_property
    def migrator(self):
        """Create migrator and setup it with fake migrations."""
        migrator = Migrator(self.database)
        for name in self.done:
            self.run_one(name, migrator)
        return migrator

    def run_one(self, name, migrator, fake=True, downgrade=False, force=False):
        """Run a migration."""

        try:
            migrate, rollback = self.read(name)
            if fake:
                with mock.patch('peewee.Model.select'):
                    with mock.patch('peewee.Query._execute'):
                        migrate(migrator, self.database, fake=fake)

                if force:
                    self.model.create(name=name)
                    self.logger.info('Done %s', name)

                migrator.clean()
                return migrator

            self.logger.info('Running "%s"', name)
            with self.database.transaction():
                if not downgrade:
                    migrate(migrator, self.database, fake=fake)
                    migrator.run()
                    self.model.create(name=name)
                    self.logger.info('Done %s', name)
                else:
                    self.logger.info('Rolling back %s', name)
                    rollback(migrator, self.database, fake=fake)
                    migrator.run()
                    self.model.delete().where(self.model.name == name).execute()
                    self.logger.info('Rolled back %s', name)

        except Exception as exc:
            self.database.rollback()
            self.logger.exception('Migration failed: %s', name)
            raise

    def run(self, name=None, fake=False):
        """Run migrations."""
        self.logger.info('Starting migrations')

        done = []
        diff = self.diff
        if not diff:
            self.logger.info('There is nothing to migrate')
            return done

        migrator = self.migrator
        for mname in diff:
            self.run_one(mname, migrator, fake=fake, force=fake)
            done.append(mname)
            if name and name == mname:
                break

        return done

    def rollback(self, name):
        name = name.strip()
        done = self.done
        if not done:
            raise RuntimeError('No migrations are found.')
        if name != done[-1]:
            raise RuntimeError('Only last migration can be canceled.')

        migrator = self.migrator
        self.run_one(name, migrator, False, True)
        self.logger.warn('Downgraded migration: %s', name)


class Router(BaseRouter):

    filemask = re.compile(r"[\d]{3}_[^\.]+\.py$")

    def __init__(self, database, migrate_dir=MIGRATE_DIR, **kwargs):
        super(Router, self).__init__(database, **kwargs)
        self.migrate_dir = migrate_dir

    @property
    def todo(self):
        """Scan migrations in file system."""
        if not os.path.exists(self.migrate_dir):
            self.logger.warn('Migration directory: %s does not exist.', self.migrate_dir)
            os.makedirs(self.migrate_dir)
        return sorted(
            ''.join(f[:-3]) for f in os.listdir(self.migrate_dir) if self.filemask.match(f))

    def _create(self, name, migrate='', rollback=''):
        """Create a migration."""
        num = len(self.todo)
        prefix = '{:03}_'.format(num + 1)
        name = prefix + name + '.py'
        path = os.path.join(self.migrate_dir, name)
        with open(path, 'w') as f:
            f.write(MIGRATE_TEMPLATE.format(migrate=migrate, rollback=rollback))

        return path

    def read(self, name):
        """Read migration from file."""
        with open(os.path.join(self.migrate_dir, name + '.py')) as f:
            code = f.read()
            scope = {}
            exec_in(code, scope)
            return scope.get('migrate', VOID), scope.get('rollback', VOID)


class ModuleRouter(BaseRouter):

    def __init__(self, database, migrate_module='migrations', **kwargs):
        super(ModuleRouter, self).__init__(database, **kwargs)

        if isinstance(migrate_module, string_types):
            migrate_module = import_module(migrate_module)

        self.migrate_module = migrate_module

    def read(self, name):
        mod = getattr(self.migrate_module, name)
        return getattr(mod, 'migrate', VOID), getattr(mod, 'rollback', VOID)
