# Copyright (c) 2018 Mycroft AI, Inc.
#
# This file is part of Mycroft Skills Kit
# (see https://github.com/MycroftAI/mycroft-skills-kit).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import atexit

import os
from configparser import NoOptionError
from contextlib import contextmanager
from difflib import SequenceMatcher
from functools import wraps
from git.config import GitConfigParser, get_config_path
from github import Github, GithubException
from github.Repository import Repository
from msm import SkillEntry
from os import chmod
from os.path import join, dirname
from tempfile import mkstemp
from typing import Optional
from glob import glob
from pathlib import Path

from msk import __version__
from msk.exceptions import PRModified, MskException, SkillNameTaken

ASKPASS = '''#!/usr/bin/env python3
print(r"""{token}""")
'''

skills_kit_footer = (
    "<sub>Created with [mycroft-skills-kit]({}) v{}</sub>".format(
        "https://github.com/mycroftai/mycroft-skills-kit", __version__
    )
)

tokendir = str(Path.home()) + "/.mycroft/msk/"
tokenfile = tokendir + "GITHUB_TOKEN"


def register_git_injector(token):
    """Generate a script that writes the token to the git command line tool"""
    fd, tmp_path = mkstemp()
    atexit.register(lambda: os.remove(tmp_path))

    with os.fdopen(fd, "w") as f:
        f.write(ASKPASS.format(token=token.replace('"""', r"\"\"\"").strip()))

    chmod(tmp_path, 0o700)
    os.environ["GIT_ASKPASS"] = tmp_path


def ask_for_github_token() -> Github:
    """Ask for GitHub Token if there isnt stored token
    or stored token is invalid"""
    print("")
    token = get_stored_github_token()
    if token and check_token(token):
        github = Github(token)
        register_git_injector(token)
        return github
    else:
        retry = False
        while True:
            if not retry:
                print(
                    "To authenticate with GitHub a Personal Access Token is "
                    "needed."
                )
                print(
                    "    1. Create one at "
                    "https://github.com/settings/tokens/new"
                )
                print("    2. Give the token a name like mycroft-msk")
                print("    3. Select the scopes")
                print("       [X] repo")
                print("    4. Click Generate Token (at bottom of page)")
                print("    5. Copy the generated token")
                print("    6. Paste it in below")
                print("")
                retry = True
            token = input("Personal Access Token: ")
            if check_token(token):
                github = Github(token)
                store_github_token(token)
                register_git_injector(token)
                return github
            else:
                print("")
                print("Token is incorrect.")
                print(
                    "The token may be missing repo scope permissions,"
                )
                print("or the token is invalid.")
                print("Please retry.")
                print("")


def check_token(token):
    """Check if at GitHub Token has 'repo' in the scope"""
    github = Github(token)
    try:
        _ = github.get_user().login
        _ = github.oauth_scopes
        if "repo" in github.oauth_scopes:
            return True
        else:
            return False
    except Exception:
        return False


def get_stored_github_token():
    """Returns stored GitHub token or false if there isnt
    one or the token is invalid"""
    if os.path.isfile(tokenfile):
        with open(tokenfile, "r") as f:
            token = f.readline()
        if not check_token(token):
            os.remove(tokenfile)
        else:
            return token
    else:
        return False


def store_github_token(token):
    """Ask if user will store GitHUb token and if yes store"""
    print("")
    if ask_yes_no(
        "Do you want msk to store the GitHub Personal Access Token? (Y/n)",
        True,
    ):
        if not os.path.exists(tokendir):
            os.makedirs(tokendir)
        with open(tokenfile, "w") as f:
            f.write(token)
            os.chmod(tokenfile, 0o600)
        print("Your GitHub Personal Access Token is stored in " + tokenfile)
        print("")
    else:
        print("Remember to store your token in a safe place.")
        print("")


def skill_repo_name(url: str):
    return "{}/{}".format(
        SkillEntry.extract_author(url), SkillEntry.extract_repo_name(url)
    )


def ask_input(message: str, validator=lambda x: True, on_fail="Invalid entry"):
    while True:
        resp = input(message + " ").strip()
        try:
            if validator(resp):
                return resp
        except Exception:
            pass
        o = on_fail(resp) if callable(on_fail) else on_fail
        if isinstance(o, str):
            print(o)


def ask_choice(
    message: str, choices: list, allow_empty=False, on_empty=None
) -> Optional[str]:
    if not choices:
        if allow_empty:
            print(on_empty)
            return None
        else:
            raise MskException(on_empty or 'Error with "{}"'.format(message))

    print()
    print(message)
    print(
        "\n".join(
            "{}. {}".format(i + 1, choice) for i, choice in enumerate(choices)
        )
    )
    print()

    def find_match(x):
        if not x and allow_empty:
            return ...
        try:
            return choices[int(x) - 1]
        except (ValueError, IndexError):
            pass

        def calc_conf(y):
            return SequenceMatcher(a=x, b=y).ratio()

        best_choice = max(choices, key=calc_conf)
        best_conf = calc_conf(best_choice)
        if best_conf > 0.8:
            return best_choice
        raise ValueError

    resp = find_match(
        ask_input(">", find_match, "Please enter one of the options.")
    )
    return None if resp is ... else resp


def ask_input_lines(message: str, bullet: str = ">") -> list:
    print(message)
    lines = []
    while len(lines) < 1 or lines[-1]:
        lines.append(ask_input(bullet))
    return lines[:-1]


def ask_yes_no(message: str, default: Optional[bool]) -> bool:
    resp = ask_input(
        message, lambda x: (not x and default is not None) or x in "yYnN"
    )
    return {"n": False, "y": True, "": default}[resp.lower()]


def create_or_edit_pr(
    title: str,
    body: str,
    skills_repo: Repository,
    user,
    branch: str,
    repo_branch: str,
):
    base = repo_branch
    head = "{}:{}".format(user.login, branch)
    pulls = list(skills_repo.get_pulls(base=base, head=head))
    if pulls:
        pull = pulls[0]
        if "mycroft-skills-kit" in pull.body:
            pull.edit(title, body)
        else:
            raise PRModified(
                "Not updating description since it was not autogenerated"
            )
        return pull
    else:
        try:
            return skills_repo.create_pull(title, body, base=base, head=head)
        except GithubException as e:
            if e.status == 422:
                raise SkillNameTaken(title) from e
            raise


def to_camel(snake):
    """time_skill -> TimeSkill"""
    return snake.title().replace("_", "")


def to_snake(camel):
    """TimeSkill -> time_skill"""
    if not camel:
        return camel
    return "".join("_" + x if "A" <= x <= "Z" else x for x in camel).lower()[
        camel[0].isupper():
    ]


@contextmanager
def print_error(exception):
    try:
        yield
    except exception as e:
        print("{}: {}".format(exception.__name__, e))


def read_file(*path):
    with open(join(*path)) as f:
        return f.read()


def read_lines(*path):
    with open(join(*path)) as f:
        return [i for i in (i.strip() for i in f.readlines()) if i]


def serialized(func):
    """Write a serializer by yielding each line of output"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        return "\n".join(
            " ".join(parts) if isinstance(parts, tuple) else parts
            for parts in func(*args, **kwargs)
        )

    return wrapper


def get_licenses():
    licenses = glob(join(dirname(__file__), "licenses", "*.txt"))
    licenses.sort()
    return licenses


GIT_IDENTITY_INFO = """=== Git Identity ===
msk uses Git to save skills to Github and when submitting a skill to the
Mycroft Marketplace. To use Git, Git needs to know your Name and
E-mail address. This is important because every Git commit uses the
information to show the responsible party for the submission.
"""


GIT_MANUAL_CHANGE_INFO = """
Thank you. :)

If you need to change this in the future use

    git --config user.name "My Name"

and

    git --config user.email "me@myhost.com"

"""


def ensure_git_user():
    """Prompt for fullname and email if git config is missing it."""
    conf_path = get_config_path("global")
    with GitConfigParser(conf_path, read_only=False) as conf_parser:

        # Make sure a user section exists
        if "user" not in conf_parser.sections():
            conf_parser.add_section("user")

        # Check for missing options using the ConfigParser and insert them
        # if they're missing.
        name, email = (None, None)
        try:
            name = conf_parser.get(section="user", option="name")
        except NoOptionError:
            pass  # Name doesn't exist deal with it later
        try:
            email = conf_parser.get(section="user", option="email")
        except NoOptionError:
            pass  # E-mail doesn't exist, deal with it later

        if not all((name, email)):
            # Some of the needed config is missing
            print(GIT_IDENTITY_INFO)
            if not name:
                name = input("Please enter Full name: ")
                conf_parser.set("user", "name", name)
            if not email:
                email = input("Please enter e-mail address: ")
                conf_parser.set("user", "email", email)

            print(GIT_MANUAL_CHANGE_INFO)
