# built-in
import os.path
import re
from logging import getLogger
from os import environ
from pathlib import Path
from typing import Dict, Set

# external
import tomlkit
from dephell_argparse import CommandHandler

# app
from ..actions import attach_deps, get_python_env
from ..cached_property import cached_property
from ..config import Config, config, get_data_dir
from ..constants import CONFIG_NAMES, ENV_VAR_TEMPLATE, GLOBAL_CONFIG_NAME
from ..controllers import analyze_conflict
from ..converters import CONVERTERS, InstalledConverter


REX_WORD = re.compile(r'([a-z\d])([A-Z])')


class BaseCommand(CommandHandler):
    logger = getLogger('dephell.commands')
    prog = 'dephell'
    find_config = True

    @cached_property
    def config(self) -> Config:
        config.setup_logging()
        self._attach_global_config_file()
        self._attach_config_file(path=self.args.config, env=self.args.env)
        config.attach_env_vars()
        config.attach_cli(self.args)
        config.setup_logging()
        return config

    def validate(self) -> bool:
        is_valid = self.config.validate()
        if not is_valid:
            self.logger.error('invalid config')
            print(self.config.format_errors())
        return is_valid

    # properties

    @cached_property
    def url(self) -> str:
        tmpl = 'https://dephell.org/docs/cmd-{}.html'
        return tmpl.format(self.name.replace(' ', '-'))

    @cached_property
    def usage(self) -> str:
        return 'dephell {} [OPTIONS]'.format(self.name)

    # helpers

    @classmethod
    def _attach_global_config_file(cls) -> bool:
        global_config = get_data_dir() / GLOBAL_CONFIG_NAME
        if not global_config.exists():
            return False
        content = global_config.read_text(encoding='utf8')
        doc = tomlkit.parse(content)
        config.attach(data=dict(doc))
        return True

    @classmethod
    def _attach_config_file(cls, path, env) -> bool:
        # get params from env vars if are not specified
        if path is None:
            path = environ.get(ENV_VAR_TEMPLATE.format('CONFIG'))
        if env is None:
            env = environ.get(ENV_VAR_TEMPLATE.format('ENV'), 'main')

        # if path to config specified explicitly, just use it
        if path:
            config.attach_file(path=path, env=env)
            return True

        # do not implicitly attach file for some commands like `jail`
        if not cls.find_config:
            cls.logger.debug('cannot find config file')
            return False

        # if path isn't specified, carefully try default names
        for path in CONFIG_NAMES:
            if not os.path.exists(path):
                continue
            data = config.attach_file(path=path, env=env, silent=True)
            if data is None:
                cls.logger.warning('cannot find tool.dephell section in the config', extra=dict(
                    path=path,
                ))
                return False
            return True

        cls.logger.warning('cannot find config file')
        return False

    def _get_locked(self, default_envs: Set[str] = None):
        if 'from' not in self.config:
            python = get_python_env(config=self.config)
            self.logger.debug('choosen python', extra=dict(path=str(python.path)))
            resolver = InstalledConverter().load_resolver(paths=python.lib_paths)
            return self._resolve(resolver=resolver, default_envs=default_envs)

        loader_config = self._get_loader_config_for_lockfile()
        if not Path(loader_config['path']).exists():
            self.logger.error('cannot find dependency file', extra=dict(path=loader_config['path']))
            return None

        self.logger.info('get dependencies', extra=dict(
            format=loader_config['format'],
            path=loader_config['path'],
        ))
        loader = CONVERTERS[loader_config['format']]
        loader = loader.copy(project_path=Path(self.config['project']))
        resolver = loader.load_resolver(path=loader_config['path'])
        attach_deps(resolver=resolver, config=self.config, merge=False)
        return self._resolve(resolver=resolver, default_envs=default_envs)

    def _resolve(self, resolver, default_envs: Set[str] = None):
        # resolve
        if len(resolver.graph._layers) <= 1:  # if it isn't resolved yet
            self.logger.info('build dependencies graph...')
            resolved = resolver.resolve(silent=self.config['silent'])
            if not resolved:
                conflict = analyze_conflict(resolver=resolver)
                self.logger.warning('conflict was found')
                print(conflict)
                return None

        # apply envs if needed
        if self.config.get('envs'):
            resolver.apply_envs(set(self.config['envs']))
        elif default_envs:
            resolver.apply_envs(default_envs)

        return resolver

    def _get_loader_config_for_lockfile(self) -> Dict[str, str]:
        # if path specified in CLI, use it
        if set(self.args.__dict__) & {'from', 'from_format', 'from_path'}:
            return self.config['from']

        dumper_config = self.config.get('to')
        if not dumper_config or dumper_config == 'stdout':
            return self.config['from']

        if not Path(dumper_config['path']).exists():
            return self.config['from']

        dumper = CONVERTERS[dumper_config['format']]
        if dumper.lock:
            return dumper_config

        return self.config['from']