# -*- coding: utf-8 -*-
"""GitHub class file."""

import datetime
import requests

from urllib.parse import urlencode


class GitHub(object):
    """GitHub class."""

    def __init__(
            self,
            token,
            org=None,
            org_id=None,
            owner_team=None,
            role_team=None,
            role_team_name=None,
            verbose=False,
            app_project=None,
    ):
        """Initialize an GitHub class instance."""
        # set github token
        self.token = token

        # set github organization name
        self.org = org
        self.org_id = org_id
        if org and not org_id:
            self.get_org_id()

        # set a team to include all owners
        self.owner_team = owner_team

        # set a team to include all role accounts
        self.role_team = role_team
        self.role_team_name = role_team_name

        # enable verbosity
        self.verbose = verbose

        # set the base urls
        self.base_url = "https://api.github.com"
        self.org_base_url = f"{self.base_url}/orgs/{self.org}"
        self.org_id_base_url = f"{self.base_url}/organizations/{self.org_id}"

        # set the headers for authorized requests
        self.headers = {"Authorization": f"token {self.token}"}

        self.app_project = app_project

    #
    # Sub Classes
    #
    def app(self, auth=None):
        """Return an App instance."""
        from bits.github.app import App
        return App(auth=auth, github=self)

    def auditlogs(self, auth=None, args=None):
        """Return an AuditLogs instance."""
        from bits.github.auditlogs import AuditLogs
        return AuditLogs(auth=auth, github=self, args=args)

    def client(self, auth=None, app_project=None):
        """Return a Client instance."""
        from bits.github.client import Client
        if not app_project:
            app_project = self.app_project
        return Client(auth=auth, github=self, app_project=app_project)

    def datastore(self, auth=None):
        """Return a Datastore instance."""
        from bits.github.datastore import Datastore
        return Datastore(auth=auth, github=self)

    def firestore(
        self,
        auth=None,
        project="broad-bitsdb-firestore",
        app_project=None,
        bitsdb_project="broad-bitsdb-prod",
    ):
        """Return a Firestore instance."""
        from bits.github.firestore import Firestore
        if not app_project:
            app_project = self.app_project
        return Firestore(
            auth=auth,
            github=self,
            project=project,
            app_project=app_project,
            bitsdb_project=bitsdb_project,
        )

    def helpers(self, auth=None):
        """Return a Firestore instance."""
        from bits.github.helpers import Helpers
        return Helpers(auth=auth, github=self)

    def sync(self, auth=None):
        """Return an Update instance."""
        from bits.github.sync import Sync
        return Sync(auth=auth, github=self)

    def update(self, auth=None):
        """Return an Update instance."""
        from bits.github.update import Update
        return Update(auth=auth, github=self)

    #
    # Helpers
    #
    def get(self, url, headers={}, params={}, raise_for_status=True):
        """Return a response from a GET call."""
        # add any additional headers
        headers = {**self.headers, **headers}

        # ret response to request
        response = requests.get(url, headers=headers, params=params)

        # raise for status
        if raise_for_status:
            response.raise_for_status()

        # return json
        return response

    def get_dict(self, items, key="id"):
        """Return a list as a dict with the given key."""
        data = {}
        for item in items:
            k = item[key]
            data[k] = item
        return data

    def get_modified(self, url, headers={}, params={}, etag=None, last_modified=None):
        """Return a response from a GET call if modified, otherwise return False."""
        # reset the headers from any previous requests
        headers["If-None-Match"] = None
        headers["If-Modified-Since"] = None

        # add etag and last-modified request headers
        if etag:
            headers["If-None-Match"] = etag
        elif last_modified:
            headers["If-Modified-Since"] = last_modified

        # get response
        response = self.get(url, headers=headers, params=params)

        # check if not changed
        if response.status_code == 304:
            return False

        return response.json()

    def get_list(self, base_url, url, headers={}, params={}):
        """Return a paginated list from a GET call."""
        items_list = []

        # add any additional headers
        headers = {**self.headers, **headers}

        next_url = f"{base_url}/{url}"

        # set page size
        if "per_page" not in params:
            params["per_page"] = 100

        while next_url:
            # get response to request
            response = requests.get(next_url, headers=headers, params=params)

            # raise for status
            response.raise_for_status()

            # get next url from response links
            next_url = response.links.get("next", {}).get("url")

            # add items to list
            items_list.extend(response.json())

        return items_list

    #
    # Login
    #
    def get_oauth_authorize_url(self, client_id, redirect_uri, scopes, state):
        """Return the oauth2 URL for linking one's GitHub account."""
        base_url = "http://github.com/login/oauth/authorize"
        params = {
            "client_id": client_id,
            "redirect_uri": redirect_uri,
            "scopes": scopes,
            "state": state,
        }
        return f"{base_url}?{urlencode(params)}"

    def get_oauth_access_token(self, client_id, client_secret, redirect_uri, code, state):
        """Return an access token for a user."""
        # POST /login/oauth/access_token
        url = "https://github.com/login/oauth/access_token"
        headers = {"Accept": "application/json"}
        params = {
            "client_id": client_id,
            "client_secret": client_secret,
            "code": code,
            "redirect_uri": f"{redirect_uri}/",
            "state": state,
        }

        # get response
        response = requests.post(url, headers=headers, params=params)

        # raise for status
        response.raise_for_status()

        # return access token
        return response.json().get("access_token")

    #
    # Organizations
    #
    def get_org(self, org=None):
        """Return an organization."""
        # GET /orgs/:org
        if not org:
            org = self.org
        url = f"{self.base_url}/orgs/{org}"
        return self.get(url).json()

    def get_org_id(self):
        """Return the org id."""
        if not self.org_id:
            self.org_id = self.get_org().get("id")
        return self.org_id

    #
    # Org Hooks
    #
    def get_org_hooks(self):
        """Return a list of organization hooks."""
        # GET /orgs/:org/hooks
        return self.get_list(self.org_base_url, "hooks")

    def get_org_hooks_dict(self, key="id"):
        """Return a dict of org hooks."""
        return self.get_dict(self.get_org_hooks(), key)

    #
    # Org Invitations
    #
    def get_org_invitations(self):
        """Return a list of organization invitations."""
        # GET /orgs/:org/invitations
        # headers = {"Accept": "application/vnd.github.dazzler-preview"}
        return self.get_list(self.org_base_url, "invitations")

    def get_org_invitations_dict(self, key="id"):
        """Return a dict of org invitations."""
        return self.get_dict(self.get_org_invitations(), key)

    def get_org_invitation_teams(self, invitation_id):
        """Return a list of organization invitation teams."""
        # GET /orgs/:org/invitations/:invitation_id/teams
        # headers = {"Accept": "application/vnd.github.dazzler-preview"}
        url = f"invitations/{invitation_id}/teams"
        return self.get_list(self.org_base_url, url)

    #
    # Org Members
    #
    def check_org_membership(self, member):
        """Return a user"s organization membership."""
        # GET /orgs/:org/members/:username
        url = f"{self.base_url}/orgs/{self.org}/members/{member}"
        response = self.get(url, raise_for_status=False).json()

        # requester is an organization member and user is a member
        if response.status_code == 204:
            return True

        # requester is an organization member and user is not a member
        if response.status_code == 404:
            return False

        # raise for status
        response.raise_for_status

        return False

    def get_org_members(self, filterString=None, role=None, insecure=False):
        """Return a list of organization members."""
        # GET /orgs/:org/members
        params = {
            "filter": filterString,
            "role": role,
        }
        # add 2fa_disabled tag if insecure is true
        if insecure:
            params["filter"] = "2fa_disabled"
        return self.get_list(self.org_base_url, "members", params=params)

    def get_org_members_dict(self, filterString=None, role=None, insecure=False, key="id"):
        """Return a dict of organization teams."""
        return self.get_dict(self.get_org_members(filterString, role, insecure), key)

    #
    # Org Memberships
    #
    def get_org_membership(self, member):
        """Return a user"s organization membership."""
        # GET /orgs/:org/memberships/:username
        url = f"{self.base_url}/orgs/{self.org}/memberships/{member}"
        return self.get(url).json()

    def invite_org_member(self, member):
        """Invite a member to the organization."""
        # PUT /orgs/:org/memberships/:username
        url = f"{self.org_base_url}/memberships/{member}"
        response = requests.put(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

    def remove_org_member(self, member):
        """Delete an invitation to the organization."""
        # DELETE /orgs/:org/memberships/:username
        url = f"{self.org_base_url}/memberships/{member}"
        return requests.delete(url, headers=self.headers)

    #
    # Org Outside Collaborators
    #
    def get_org_outside_collaborators(self, filterString=None, insecure=False):
        """Return a list of organization outside collaborators."""
        # GET /orgs/:org/outside_collaborators
        params = {"filter": filterString}
        if insecure:
            params["filter"] = "2fa_disabled"
        return self.get_list(self.org_base_url, "outside_collaborators", params=params)

    def get_org_outside_collaborators_dict(self, filterString=None, insecure=False, key="id"):
        """Return a dict of organization outside collaborators."""
        return self.get_dict(self.get_org_outside_collaborators(filterString, insecure), key)

    #
    # Org Public Members
    #
    def get_org_public_member(self, member):
        """Check if a user is a public member of the org."""
        # GET /orgs/:org/public_members/:username
        url = f"{self.base_url}/orgs/{self.org}/public_members/{member}"
        return self.get(url)

    def get_org_public_members(self):
        """Return a list of organization public members."""
        # GET /orgs/:org/public_members
        return self.get_list(self.org_base_url, "public_members")

    def get_org_public_members_dict(self, key="id"):
        """Return a dict of org public members."""
        return self.get_dict(self.get_org_public_members(), key)

    #
    # Org Repos
    #
    def get_org_repos(self):
        """Return a list of organization repos."""
        # GET /orgs/:org/repos
        headers = {"Accept": "application/vnd.github.baptiste-preview+json"}
        return self.get_list(self.org_base_url, "repos", headers=headers)

    def get_org_repos_dict(self, key="id"):
        """Return a dict of org repos."""
        return self.get_dict(self.get_org_repos(), key)

    #
    # Org Teams
    #
    def get_org_team(self, team_slug, etag=None, last_modified=None):
        """Return a team by slug."""
        # GET /orgs/{org}/teams/{team_slug}
        url = f"{self.org_base_url}/teams/{team_slug}"
        return self.get_modified(url, etag=etag, last_modified=last_modified)

    def get_org_team_by_id(self, team_id, etag=None, last_modified=None):
        """Return a team by ID."""
        # GET /organizations/{org_id}/team/{team_id}
        url = f"{self.org_id_base_url}/team/{team_id}"
        return self.get_modified(url, etag=etag, last_modified=last_modified)

    def get_org_team_invitations(self, team_slug):
        """Return a list of org team invitations by slug."""
        # GET /orgs/{org}/teams/{team_slug}/invitations
        url = f"teams/{team_slug}/invitations"
        return self.get_list(self.org_base_url, url)

    def get_org_team_invitations_by_id(self, team_id):
        """Return a list of org team invitations by ID."""
        # GET /organizations/{org_id}/team/{team_id}/invitations
        url = f"team/{team_id}/invitations"
        return self.get_list(self.org_id_base_url, url)

    def get_org_teams(self):
        """Return a list of organization teams."""
        # GET /orgs/:org/teams
        # add header for Nested Teams API preview
        return self.get_list(self.org_base_url, "teams")

    def get_org_teams_dict(self, key="id"):
        """Return a dict of organization teams."""
        return self.get_dict(self.get_org_teams(), key)

    def get_org_team_hierarchy(self):
        """Return a dict of organization teams and their children."""
        organization = {}
        teams = self.get_org_teams()
        for team in sorted(teams, key=lambda x: x.get("parent")):
            tid = team["id"]
            parent = team["parent"]
            if not parent:
                team["children"] = []
                organization[tid] = team
            else:
                pid = parent["id"]
                if pid not in organization:
                    print(f"ERROR: Parent ID not found: {pid}")
                else:
                    organization[pid]["children"].append(team)
        return organization

    def get_org_team_members(self, team_slug, role="all"):
        """Return a list of organization team members by slug."""
        # GET /orgs/{org}/teams/{team_slug}/members
        url = f"teams/{team_slug}/members"
        params = {"role": role}
        return self.get_list(self.org_base_url, url, params=params)

    def get_org_team_members_by_id(self, team_id, role="all"):
        """Return a list of organization team members by ID."""
        # GET /organizations/{org_id}/team/{team_id}/members
        url = f"team/{team_id}/members"
        params = {"role": role}
        return self.get_list(self.org_id_base_url, url, params=params)

    def get_org_team_repos(self, team_slug):
        """Return a list of organization team repos by slug."""
        # GET /orgs/{org}/teams/{team_slug}/repos
        url = f"teams/{team_slug}/repos"
        return self.get_list(self.org_base_url, url)

    def get_org_team_repos_by_id(self, team_id):
        """Return a list of organization team repos by ID."""
        # GET /organizations/{org_id}/team/{team_id}/repos
        url = f"team/{team_id}/repos"
        return self.get_list(self.org_base_url, url)

    def invite_org_team_member(self, team_slug, username):
        """Invite a user to an organization team by slug."""
        # PUT /orgs/{org}/teams/{team_slug}/memberships/{username}
        url = f"{self.org_base_url}/teams/{team_slug}/memberships/{username}"
        params = {"role": "member"}
        return requests.put(url, headers=self.headers, params=params)

    def invite_org_team_member_by_id(self, team_id, username):
        """Invite a user to an organization team by ID."""
        # PUT /organizations/{org_id}/team/{team_id}/memberships/{username}
        url = f"{self.org_id_base_url}/team/{team_id}/memberships/{username}"
        params = {"role": "member"}
        return requests.put(url, headers=self.headers, params=params)

    def remove_org_team_member(self, team_slug, username):
        """Remove a user from an organization team by slug."""
        # DELETE /orgs/{org}/teams/{team_slug}/memberships/{username}
        url = f"{self.org_base_url}/teams/{team_slug}/memberships/{username}"
        return requests.delete(url, headers=self.headers)

    def remove_org_team_member_by_id(self, team_id, username):
        """Remove a user from an organization team by ID."""
        # DELETE /organizations/{org_id}/team/{team_id}/memberships/{username}
        url = f"{self.org_id_base_url}/team/{team_id}/memberships/{username}"
        return requests.delete(url, headers=self.headers)

    #
    # Rate Limit
    #
    def get_rate_limit(self):
        """Return a repo of the organization."""
        # GET /rate_limit
        url = f"{self.base_url}/rate_limit"
        data = requests.get(url, headers=self.headers).json()

        # get rate information
        rate = data["rate"]
        limit = rate["limit"]
        remaining = rate["remaining"]
        reset = rate["reset"]

        # calculate when the quota will reset
        reset_time = datetime.datetime.fromtimestamp(reset)
        now = datetime.datetime.now()
        delta = reset_time - now
        print(f"{remaining} remaining of {limit} limit, resets in {delta.seconds} seconds at: {reset_time}")

        return data

    #
    # Repos
    #
    def add_repo_collaborator(self, repo, username, permission="pull"):
        """Add a user as a collaborator to a repo."""
        # PUT /repos/:owner/:repo/collaborators/:username
        url = f"{self.base_url}/repos/{self.org}/{repo}/collaborators/{username}"
        params = {
            "permission": permission,
        }
        return requests.put(url, headers=self.headers, params=params)

    def get_repo(self, repo, etag=None, last_modified=None):
        """Return a repo of the organization."""
        # GET /repos/:owner/:repo
        url = f"{self.base_url}/repos/{self.org}/{repo}"
        return self.get_modified(url, etag=etag, last_modified=last_modified)

    def get_repo_collaborators(self, repo, affiliation=None):
        """Return a list of repo hook."""
        # GET /repos/:owner/:repo/collaborators
        url = f"repos/{self.org}/{repo}/collaborators"
        params = {"affiliation": affiliation}
        return self.get_list(self.base_url, url, params=params)

    def get_repo_collaborators_dict(self, repo, affiliation=None, key="id"):
        """Return a dict of org repo collaborators."""
        return self.get_dict(self.get_repo_collaborators(repo, affiliation), key)

    def get_repo_hooks(self, repo):
        """Return a list of repo hook."""
        # GET /repos/:owner/:repo/hooks
        url = f"repos/{self.org}/{repo}/hooks"
        return self.get_list(self.base_url, url)

    def get_repo_hooks_dict(self, repo, key="id"):
        """Return a dict of org hooks."""
        return self.get_dict(self.get_repo_hooks(repo), key)

    def update_repo_hook(self, repo, hook_id, body):
        """Update a repo hook."""
        # PATCH /repos/:owner/:repo/hooks/:hook_id
        url = f"{self.base_url}/repos/{self.org}/{repo}/hooks/{hook_id}"
        return requests.patch(url, headers=self.headers, json=body)

    #
    # Teams
    #
    def get_team(self, team_id, etag=None, last_modified=None):
        """Return a team of the organization."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#get-a-team-legacy
        # GET /teams/:team_id
        # url = "{}/teams/{}".format(self.base_url, team_id)
        # return self.get_modified(url, etag=etag, last_modified=last_modified)
        return self.get_org_team_by_id(team_id, etag=etag, last_modified=last_modified)

    def get_team_invitations(self, team_id):
        """Return a list of team invitations."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#list-pending-team-invitations-legacy
        # GET /teams/:team_id/invitations
        # headers = {"Accept": "application/vnd.github.dazzler-preview+json"}
        # url = "teams/{}/invitations".format(team_id)
        # return self.get_list(self.base_url, url, headers=headers)
        return self.get_org_team_invitations_by_id(team_id)

    def get_team_invitations_dict(self, team_id, key="id"):
        """Return a dict of team invitations."""
        return self.get_dict(self.get_team_invitations(team_id), key)

    def get_team_members(self, team_id, role="all"):
        """Return a list of team members."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#list-team-members-legacy
        # GET /teams/:team_id/members
        # headers = {"Accept": "application/vnd.github.hellcat-preview+json"}
        # params = {"role": role}
        # url = "teams/{}/members".format(team_id)
        # return self.get_list(self.base_url, url, params=params)
        return self.get_org_team_members_by_id(team_id, role=role)

    def get_team_members_dict(self, team_id, role="all", key="id"):
        """Return a dict of team members."""
        return self.get_dict(self.get_team_members(team_id, role), key)

    def get_team_members_with_children(self, team_id, role="all"):
        """Return a list of team members with children."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#list-team-members-legacy
        # GET /teams/:team_id/members
        # headers = {"Accept": "application/vnd.github.hellcat-preview+json"}
        # params = {"role": role}
        # url = "teams/{}/members".format(team_id)
        # return self.get_list(self.base_url, url, headers=headers, params=params)
        return self.get_org_team_members_by_id(team_id, role=role)

    def get_team_members_with_children_dict(self, team_id, role="all", key="id"):
        """Return a dict of team members with children."""
        return self.get_dict(self.get_team_members_with_children(team_id, role), key)

    def invite_team_member(self, team_id, username):
        """Invite a member to the organization."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#add-or-update-team-membership-for-a-user-legacy
        # PUT /teams/:id/memberships/:username
        # params = {"role": "member"}
        # url = "{}/teams/{}/memberships/{}".format(self.base_url, team_id, username)
        # return requests.put(url, headers=self.headers, params=params)
        return self.invite_org_team_member_by_id(team_id, username)

    def remove_team_member(self, team_id, username):
        """Remove a member from the organization."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#remove-team-membership-for-a-user-legacy
        # DELETE /teams/:id/memberships/:username
        # url = "{}/teams/{}/memberships/{}".format(self.base_url, team_id, username)
        # return requests.delete(url, headers=self.headers)
        return self.remove_org_team_member_by_id(team_id, username)

    def get_team_repos(self, team_id):
        """Return a list of team repos."""
        # Deprecated: https://docs.github.com/en/rest/reference/teams#list-team-repositories-legacy
        # GET /teams/:team_id/repos
        # Add header to get the Nested Teams API
        # headers = {"Accept": "application/vnd.github.hellcat-preview+json"}
        # url = "teams/{}/repos".format(team_id)
        # return self.get_list(self.base_url, url)
        return self.get_org_team_repos_by_id(team_id)

    def get_team_repos_dict(self, team_id, key="id"):
        """Return a dict of team repos."""
        return self.get_dict(self.get_team_repos(team_id), key)

    #
    # User (self - logged in user)
    #
    def get_self(self, etag=None, last_modified=None):
        """Return the logged in user."""
        # GET /user
        url = f"{self.base_url}/user"
        return self.get_modified(url, etag=etag, last_modified=last_modified)

    def get_self_repos(self):
        """Return a list of user repos."""
        # GET /user/repos
        url = "user/repos"
        return self.get_list(self.base_url, url)

    def get_self_repos_dict(self, key="id"):
        """Return a dict of user repos."""
        return self.get_dict(self.get_self_repos(), key)

    def get_self_teams(self):
        """Return a list of teams for the logged-in user."""
        # GET /user/teams - requires "user" or "repo" scope
        url = "user/teams"
        return self.get_list(self.base_url, url)

    def get_self_teams_dict(self, key="id"):
        """Return a dict of team teams."""
        return self.get_dict(self.get_self_teams(), key)

    #
    # Users
    #
    def get_user(self, login, etag=None, last_modified=None):
        """Return a single user."""
        # GET /users/:username
        url = f"{self.base_url}/users/{login}"
        return self.get_modified(url, etag=etag, last_modified=last_modified)

    def get_user_repos(self, login):
        """Return a list of public repositories for the specified user."""
        # GET /users/:username/repos
        url = f"users/{login}/repos"
        return self.get_list(self.base_url, url)

    def get_user_repos_dict(self, login, key="id"):
        """Return a dict of public repositories for the specified user."""
        return self.get_dict(self.get_user_repos(login), key)
