'''
Even though this command doesn't require Django to be setup,
it should run after django.setup() just to make sure it doesn't
crash when pushed to Heroku
'''

from .base import BaseCommand
import tarfile
import os
import logging
from pathlib import Path
import sys
import re
from otree import __version__ as otree_version

logger = logging.getLogger(__name__)

# need to resolve to expand path
PROJECT_PATH = Path('.').resolve()

# don't want to use the .gitignore format, it looks like a mini-language
# https://git-scm.com/docs/gitignore#_pattern_format

# TODO: maybe some of these extensions like .env, staticfiles could legitimately exist in subfolders.
EXCLUDED_PATH_ENDINGS = '~ .git db.sqlite3 .pyo .pyc .pyd .idea .DS_Store .otreezip venv _static_root staticfiles __pycache__ .env'.split()

OVERWRITE_TOKEN = 'oTree-may-overwrite-this-file'
DONT_OVERWRITE_TOKEN = 'oTree-may-not-overwrite-this-file'


def filter_func(tar_info: tarfile.TarInfo):
    path = tar_info.path

    for ending in EXCLUDED_PATH_ENDINGS:
        if path.endswith(ending):
            return None

    if '__temp' in path:
        return None

    # size is in bytes
    kb = tar_info.size >> 10
    if kb > 500:
        logger.info(f'Adding large file ({kb} KB): {path}')

    # make sure all dirs are writable, so their children can be deleted,
    # so that otree unzip/zipserver work as expected.
    # we were getting some folders with permission 16749.
    tar_info.mode |= 0o222
    return tar_info


class Command(BaseCommand):
    help = "Zip into an archive"

    def handle(self, **options):
        zip_project(PROJECT_PATH)

    def run_from_argv(self, argv):
        '''
        copy-pasted from 'unzip' command
        '''

        parser = self.create_parser(argv[0], argv[1])
        options = parser.parse_args(argv[2:])
        cmd_options = vars(options)
        self.handle(**cmd_options)


def zip_project(project_path: Path):
    # always use the same name for simplicity and so that we don't get bloat
    # or even worse, all the previous zips being included in this one
    # call it zipped.tar so that it shows up alphabetically last
    # (using __temp prefix makes it show up in the middle, because it's a file)
    archive_name = f'{project_path.name}.otreezip'

    settings_file = project_path / 'settings.py'
    if not settings_file.exists():
        msg = (
            "Cannot find oTree settings. "
            "You must run this command from the folder that contains your "
            "settings.py file."
        )
        logger.error(msg)
        sys.exit(1)

    for fn, new_text in fix_reqs_files(project_path).items():
        project_path.joinpath(fn).write_text(new_text)

    try:
        validate_reqs_files(project_path)
    except RequirementsError as exc:
        logger.error(str(exc))
        sys.exit(1)

    # once Heroku uses py 3.7 by default, we can remove this runtime stuff.
    runtime_txt = project_path / 'runtime.txt'
    runtime_existed = runtime_txt.exists()
    if not runtime_existed:
        # don't use sys.version_info because it might be newer than what
        # heroku supports
        runtime_txt.write_text(f'python-3.7.7')
    try:
        with tarfile.open(archive_name, 'w:gz') as tar:
            # if i omit arcname, it nests the project 2 levels deep.
            # if i say arcname=proj, it puts the whole project in a folder.
            # if i say arcname='', it has 0 levels of nesting.
            tar.add(project_path, arcname='', filter=filter_func)
    finally:
        if not runtime_existed:
            runtime_txt.unlink()
    logger.info(f'Saved your code into file "{archive_name}"')


def fix_reqs_files(project_path: Path) -> dict:
    rpath = project_path.joinpath('requirements.txt')
    rbpath = project_path.joinpath('requirements_base.txt')
    original_rtxt = rpath.read_text('utf8')
    original_rbtxt = rbpath.read_text('utf8') if rbpath.exists() else ''

    can_overwrite = False
    if OVERWRITE_TOKEN in original_rtxt:
        can_overwrite = True
    elif DONT_OVERWRITE_TOKEN not in original_rtxt:
        ans = input(
            "Do you want oTree to automatically keep your requirements files up to date?\n"
            "(Enter 'n' if you have custom requirements in requirements.txt or requirements_base.txt)\n"
            "(y/n): "
        ).lower()
        if ans == 'y':
            can_overwrite = True
        elif ans == 'n':
            return {rpath.name: f'# {DONT_OVERWRITE_TOKEN}\n' + original_rtxt}
        else:
            sys.stdout.write('Answer not recognized; skipping\n')
            can_overwrite = False

    if can_overwrite:
        txt = [REQS_DEFAULT, REQS_DEFAULT_MTURK][
            'otree[mturk]' in (original_rtxt + original_rbtxt)
        ]
        d = {rpath.name: txt}
        if rbpath.exists():
            d[rbpath.name] = REQS_BASE_DEFAULT
        return d
    else:
        return {}


def get_non_comment_lines(f):
    lines = []
    for line in f:
        line = line.strip()
        if line and not line.startswith('#'):
            lines.append(line)
    return lines


class RequirementsError(Exception):
    pass


REQS_BASE_DEFAULT = '''\
# You should put your requirements in requirements.txt instead.
# You can delete this file.
'''

# we do otree>= because if we require the exact version,
# then if you upgrade and run devserver, otree will complain
# that you are using the wrong version.
# if someone needs that exact version, they can manage the file manually.
_REQS_DEFAULT_FMT = f'''\
# {OVERWRITE_TOKEN}
# IF YOU MODIFY THIS FILE, remove these comments. 
# otherwise, oTree will automatically overwrite it.
otree%s>={otree_version}
psycopg2>=2.8.4
sentry-sdk>=0.7.9
'''

REQS_DEFAULT = _REQS_DEFAULT_FMT % ''
REQS_DEFAULT_MTURK = _REQS_DEFAULT_FMT % '[mturk]'


def validate_reqs_files(project_path: Path):
    rpath = project_path / 'requirements.txt'
    rbpath = project_path / 'requirements_base.txt'

    with rpath.open(encoding='utf8') as f:
        rlines = get_non_comment_lines(f)

    if rbpath.exists():
        with rbpath.open(encoding='utf8') as f:
            rblines = get_non_comment_lines(f)

        # check duplicates
        already_seen = set()
        for ln in rlines + rblines:
            m = re.match(r'(^[\w-]+).*?', ln)
            if m:
                package = m.group(1)
                if package in already_seen:
                    msg = (
                        f'"{package}" is listed more than once '
                        'in your requirements_base.txt & requirements.txt. '
                    )
                    raise RequirementsError(msg)
                already_seen.add(package)
    else:
        REFERENCE_TO_REQS_BASE = '-r requirements_base.txt'
        if REFERENCE_TO_REQS_BASE in rlines:
            msg = f'your requirements.txt has a line that says "{REFERENCE_TO_REQS_BASE}". You should remove that line.'
            raise RequirementsError(msg)

    # better to tell people about this so they stop deleting that line.
    # also simpler to implement and test the warning
    if not 'psycopg2' in rpath.read_text('utf8'):
        msg = 'You should add a line to your requirements.txt that says: psycopg2'
        raise RequirementsError(msg)
