import os
import re
import io
import sys
import json
import argparse
import unittest
from datetime import date
from getpass import getpass

try:
    import github3
except ImportError:
    print('To run release tool you need to install github3.py:')
    print('')
    print('  $ pip install github3.py')
    print('')
    sys.exit(1)


AREA_RENAME = {
    'api': 'API',
    'dht': 'DHT'
}


def get_github():
    config_path = os.path.expanduser('~/.lbry-release-tool.json')
    if os.path.exists(config_path):
        with open(config_path, 'r') as config_file:
            config = json.load(config_file)
            return github3.login(token=config['token'])

    token = os.environ.get("GH_TOKEN")
    if not token:
        print('GitHub Credentials')
        username = input('username: ')
        password = getpass('password: ')
        gh = github3.authorize(
            username, password, ['repo'], 'lbry release tool',
            two_factor_callback=lambda: input('Enter 2FA: ')
        )
        with open(config_path, 'w') as config_file:
            json.dump({'token': gh.token}, config_file)
        token = gh.token
    return github3.login(token=token)


def get_labels(pr, prefix):
    for label in pr.labels:
        label_name = label['name']
        if label_name.startswith(f'{prefix}: '):
            yield label_name[len(f'{prefix}: '):]


def get_label(pr, prefix):
    for label in get_labels(pr, prefix):
        return label


BACKWARDS_INCOMPATIBLE = 'backwards-incompatible:'
RELEASE_TEXT = "release-text:"


def get_backwards_incompatible(desc: str):
    for line in desc.splitlines():
        if line.startswith(BACKWARDS_INCOMPATIBLE):
            yield line[len(BACKWARDS_INCOMPATIBLE):]


def get_release_text(desc: str):
    for line in desc.splitlines():
        if line.startswith(RELEASE_TEXT):
            yield line[len(RELEASE_TEXT):]


def get_previous_final(repo, current_release):
    assert current_release.rc is not None, "Need an rc to find the previous final release."
    previous = None
    for tag in repo.tags(current_release.rc+1):
        previous = tag
    return previous


class Version:

    def __init__(self, major=0, minor=0, micro=0, rc=None):
        self.major = int(major)
        self.minor = int(minor)
        self.micro = int(micro)
        self.rc = rc if rc is None else int(rc)

    @classmethod
    def from_string(cls, version_string):
        (major, minor, micro), rc = version_string.split('.'), None
        if 'rc' in micro:
            micro, rc = micro.split('rc')
        return cls(major, minor, micro, rc)

    @classmethod
    def from_content(cls, content):
        src = content.decoded.decode('utf-8')
        version = re.search('__version__ = "(.*?)"', src).group(1)
        return cls.from_string(version)

    def increment(self, action):
        cls = self.__class__

        if action == '*-rc':
            assert self.rc is not None, f"Can't drop rc designation because {self} is already not an rc."
            return cls(self.major, self.minor, self.micro)
        elif action == '*+rc':
            assert self.rc is not None, "Must already be an rc to increment."
            return cls(self.major, self.minor, self.micro, self.rc+1)

        assert self.rc is None, f"Can't start a new rc because {self} is already an rc."
        if action == 'major+rc':
            return cls(self.major+1, rc=1)
        elif action == 'minor+rc':
            return cls(self.major, self.minor+1, rc=1)
        elif action == 'micro+rc':
            return cls(self.major, self.minor, self.micro+1, 1)

        raise ValueError(f'unknown action: {action}')

    @property
    def tag(self):
        return f'v{self}'

    def __str__(self):
        version = '.'.join(str(p) for p in [self.major, self.minor, self.micro])
        if self.rc is not None:
            version += f'rc{self.rc}'
        return version


def release(args):
    gh = get_github()
    repo = gh.repository('lbryio', 'lbry-sdk')
    version_file = repo.file_contents('lbry/lbry/__init__.py')

    current_version = Version.from_content(version_file)
    print(f'Current Version: {current_version}')
    new_version = current_version.increment(args.action)
    print(f'    New Version: {new_version}')

    if args.action == '*-rc':
        previous_release = repo.release_from_tag(args.start_tag or get_previous_final(repo, current_version))
    else:
        previous_release = repo.release_from_tag(current_version.tag)

    print(f' Changelog From: {previous_release.tag_name} ({previous_release.created_at})')
    print()

    incompats = []
    release_texts = []
    unlabeled = []
    areas = {}
    for pr in gh.search_issues(f"merged:>={previous_release._json_data['created_at']} repo:lbryio/lbry-sdk"):
        area_labels = list(get_labels(pr, 'area'))
        type_label = get_label(pr, 'type')
        if area_labels and type_label:
            for area_name in area_labels:
                for incompat in get_backwards_incompatible(pr.body):
                    incompats.append(f'  * [{area_name}] {incompat.strip()} ({pr.html_url})')
                for release_text in get_release_text(pr.body):
                    release_texts.append(f'{release_text.strip()} ({pr.html_url})')
                if not (args.action == '*-rc' and type_label == 'fixup'):
                    area = areas.setdefault(area_name, [])
                    area.append(f'  * [{type_label}] {pr.title} ({pr.html_url}) by {pr.user["login"]}')
        else:
            unlabeled.append(f'  * {pr.title} ({pr.html_url}) by {pr.user["login"]}')

    area_names = list(areas.keys())
    area_names.sort()

    body = io.StringIO()
    w = lambda s: body.write(s+'\n')

    w(f'## [{new_version}] - {date.today().isoformat()}')
    if release_texts:
        w('')
        for release_text in release_texts:
            w(release_text)
            w('')
    if incompats:
        w('')
        w(f'### Backwards Incompatible Changes')
        for incompat in incompats:
            w(incompat)
    for area in area_names:
        prs = areas[area]
        area = AREA_RENAME.get(area.lower(), area.capitalize())
        w('')
        w(f'### {area}')
        for pr in prs:
            w(pr)

    print(body.getvalue())

    if unlabeled:
        print('The following PRs were skipped and not included in changelog:')
        for skipped in unlabeled:
            print(skipped)

    if not args.dry_run:

        commit = version_file.update(
          new_version.tag,
          version_file.decoded.decode('utf-8').replace(str(current_version), str(new_version)).encode()
        )['commit']

        repo.create_tag(
            tag=new_version.tag,
            message=new_version.tag,
            sha=commit.sha,
            obj_type='commit',
            tagger=commit.committer
        )

        repo.create_release(
            new_version.tag,
            name=new_version.tag,
            body=body.getvalue(),
            draft=True,
            prerelease=new_version.rc is not None
        )


class TestReleaseTool(unittest.TestCase):

    def test_version_parsing(self):
        self.assertTrue(str(Version.from_string('1.2.3')), '1.2.3')
        self.assertTrue(str(Version.from_string('1.2.3rc4')), '1.2.3rc4')

    def test_version_increment(self):
        v = Version.from_string('1.2.3')
        self.assertTrue(str(v.increment('major+rc')), '2.0.0rc1')
        self.assertTrue(str(v.increment('minor+rc')), '1.3.0rc1')
        self.assertTrue(str(v.increment('micro+rc')), '1.2.4rc1')
        with self.assertRaisesRegex(AssertionError, "Must already be an rc to increment."):
            v.increment('*+rc')
        with self.assertRaisesRegex(AssertionError, "Can't drop rc designation"):
            v.increment('*-rc')

        v = Version.from_string('1.2.3rc3')
        self.assertTrue(str(v.increment('*+rc')), '1.2.3rc4')
        self.assertTrue(str(v.increment('*-rc')), '1.2.3')
        with self.assertRaisesRegex(AssertionError, "already an rc"):
            v.increment('major+rc')
        with self.assertRaisesRegex(AssertionError, "already an rc"):
            v.increment('minor+rc')
        with self.assertRaisesRegex(AssertionError, "already an rc"):
            v.increment('micro+rc')


def test():
    runner = unittest.TextTestRunner(verbosity=2)
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromTestCase(TestReleaseTool)
    runner.run(suite)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--test", default=False, action="store_true", help="run unit tests")
    parser.add_argument("--dry-run", default=False, action="store_true", help="show what will be done")
    parser.add_argument("--start-tag", help="custom starting tag for changelog generation")
    parser.add_argument("action", nargs="?", choices=['major+rc', 'minor+rc', 'micro+rc', '*+rc', '*-rc'])
    args = parser.parse_args()
    if args.test:
        test()
    else:
        release(args)


if __name__ == "__main__":
    main()