import asyncio
import datetime
import sys
import time
import os

import plugins.basetypes
import plugins.configuration
import plugins.database
import plugins.repositories
import plugins.projects
import plugins.github
import plugins.ldap
import aiohttp
import yaml

class ProgTimer:
    """A simple task timer that displays when a sub-task is begun, ends, and the time taken."""

    def __init__(self, title):
        self.title: str = title
        self.time: float = time.time()

    async def __aenter__(self):
        sys.stdout.write("[%s] %s...\n" % (datetime.datetime.now().strftime("%H:%M:%S"), self.title))
        sys.stdout.flush()
        self.start = time.time()

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("[%s] Done in %.2f seconds" % (datetime.datetime.now().strftime("%H:%M:%S"), time.time() - self.start))


async def adjust_teams(server: plugins.basetypes.Server):
    """Adjusts GitHub teams:
    - Adds new LDAP members with MFA enabled
    - Removes members no longer in LDAP or no longer with MFA enabled
    """
    async with ProgTimer("Adjusting GitHub teams according to LDAP/MFA"):
        for team in server.data.teams:
            if team.type == "committers":
                asf_project = server.data.projects.get(team.project)
                if asf_project:
                    if asf_project.public_repos:
                        ldap_github_team = asf_project.public_github_team(server.data.mfa)
                        if asf_project.committers:  # Only set if we got LDAP data back
                            added, removed = await team.set_membership(ldap_github_team)
                            if added:
                                print(f"Added {len(added)} members to team {team.slug}: {', '.join(added)}")
                            if removed:
                                print(f"Removed {len(removed)} members from team {team.slug}: {', '.join(removed)}")
                else:
                    print(f"Could not find an ASF project for team {team.slug}!!")

            # PMC Groups
            elif team.type == "private":
                asf_project = server.data.projects.get(team.project)
                if asf_project:
                    if asf_project.private_repos:
                        ldap_github_team = asf_project.private_github_team(server.data.mfa)
                        if asf_project.pmc:  # Only set if we got LDAP data back
                            added, removed = await team.set_membership(ldap_github_team)
                            if added:
                                print(f"Added {len(added)} members to team {team.slug}: {', '.join(added)}")
                            if removed:
                                print(f"Removed {len(removed)} members from team {team.slug}: {', '.join(removed)}")
                else:
                    print(f"Could not find an ASF project for team {team.slug}!!")


async def adjust_repositories(server: plugins.basetypes.Server):
    """Adjusts repositories, adding/removing disparities between GitBox and GitHub"""
    async with ProgTimer("Adjusting GitHub team repositories according to gitbox repos"):
        for team in server.data.teams:
            if team.type == "committers":
                asf_project = server.data.projects.get(team.project)
                if asf_project:
                    managed_repos = [
                        x.filename for x in asf_project.public_repos if x.filename in server.data.github_repos
                    ]
                    added, removed = await team.set_repositories(managed_repos)
                    for repo in added:
                        print(f"- Added {repo}.git to GitHub team {team.slug}")
                    for repo in removed:
                        print(f"- Removed {repo}.git from GitHub team {team.slug}")
            elif team.type == "private":
                asf_project = server.data.projects.get(team.project)
                if asf_project:
                    managed_repos = [
                        x.filename for x in asf_project.private_repos if x.filename in server.data.github_repos
                    ]
                    added, removed = await team.set_repositories(managed_repos)
                    for repo in added:
                        print(f"- Added {repo}.git to GitHub team {team.slug}")
                    for repo in removed:
                        print(f"- Removed {repo}.git from GitHub team {team.slug}")


async def run_tasks(server: plugins.basetypes.Server):
    """
        Runs long-lived background data gathering tasks such as gathering repositories, projects and ldap/mfa data.

        Generally runs every 2½ minutes, or whatever is set in tasks/refresh_rate in boxer.yaml
    """

    while True:
        now = time.time()
        print(f"Processing GitHub organization '{server.config.github.org}'...")
        asf_github_org = plugins.github.GitHubOrganisation(
            login=server.config.github.org, personal_access_token=server.config.github.token
        )
        await asf_github_org.get_id()  # For security reasons, we must call this before we can add/remove members
        limit, used, resets = await asf_github_org.rate_limit_rest()
        while used >= (limit-25):
            how_long_to_wait = resets - int(time.time()-1)
            print("GitHub REST rate limit reached, waiting till %d (%d seconds)" % (resets, how_long_to_wait))
            await asyncio.sleep(how_long_to_wait)
            limit, used, resets = await asf_github_org.rate_limit_rest()
        print("Used %d out of %d REST tokens this hour." % (used, limit))
        limit, used, resets = await asf_github_org.rate_limit_graphql()
        while used >= (limit-25):
            how_long_to_wait = resets - int(time.time()-1)
            print("GitHub GraphQL rate limit reached, waiting till %d (%d seconds)" % (resets, how_long_to_wait))
            await asyncio.sleep(how_long_to_wait)
            limit, used, resets = await asf_github_org.rate_limit_graphql()
        print("Used %d out of %d GraphQL tokens this hour." % (used, limit))

        async with ProgTimer("Gathering list of repositories on gitbox"):
            try:
                server.data.repositories = await plugins.repositories.list_all(server.config.repos)
            except Exception as e:
                print("Could not fetch repositories - source server down or not connected: %s" % e)
                await asyncio.sleep(10)
                continue

        try:
            async with ProgTimer("Gathering MFA status of GitHub users"):
                server.data.mfa = await asf_github_org.get_mfa_status()
        except KeyError:  # Bad github response, graphql down?
            if not server.data.mfa: # No original data, we can't continue!
                print("Could not grab MFA data from GitHub's GraphQL interface and I have no MFA history, need to abort and try later!")
                continue
       
        async with ProgTimer("Gathering list of repositories on GitHub"):
            server.data.github_repos = await asf_github_org.load_repositories()
            print(f"Found {len(server.data.github_repos)} repositories on GitHub")
        async with ProgTimer("Compiling list of projects, repos and memberships"):
            try:
                asf_org = await plugins.projects.compile_data(
                    server.config.ldap, server.data.repositories, server.database.client
                )
                server.data.projects = asf_org.projects
                for person in asf_org.committers:
                    # Append?
                    if person not in server.data.people:
                        server.data.people.append(person)
                    # Update?
                    else:
                        for p in server.data.people:
                            if p.asf_id == person.asf_id:
                                p.repositories = person.repositories
                                p.projects = person.projects
                                break
            except Exception as e:
                print("Could not fetch repositories - ldap source down or not connected: %s" % e)

        async with ProgTimer("Adjusting MFA status for users"):
            for person in server.data.people:
                if person.github_login and person.github_login in server.data.mfa:
                    if person.github_mfa is not server.data.mfa[person.github_login]:
                        person.github_mfa = server.data.mfa[person.github_login]
                        person.save(server.database.client)  # Update sqlite db if changed
                else:
                    person.github_mfa = False  # Flag as no MFA if person was not found

        async with ProgTimer("Getting GitHub teams and their members"):
            try:
                tmp_teams = await asf_github_org.load_teams()
                server.data.teams = tmp_teams
            except (AssertionError, TypeError) as e:
                print("Invalid response from GitHub while trying to fetch latest teams, will use cached response:")
                print(e)
        if server.data.teams: 
            async with ProgTimer("Looking for missing/invalid GitHub teams"):
                try:
                    await asf_github_org.setup_teams(server.data.projects)
                except AssertionError as e:
                    print("Got an AssertionError while trying to set up GitHub teams, will try again later:")
                    print(e)

            async with ProgTimer("Fetching latest LDAP data via Whimsy"):
                try:
                    session_timeout = aiohttp.ClientTimeout(total=None, sock_connect=15, sock_read=15)
                    url = "https://whimsy.apache.org/public/public_ldap_projects.json"
                    async with aiohttp.client.ClientSession(timeout=session_timeout) as hc:
                        rv = await hc.get(url)
                        js = await rv.json()
                        podlings = []
                        for project, data in js['projects'].items():
                            server.data.pmcs[project] = data.get("owners", [])
                            if data.get("podling") == "current":
                                podlings.append(project)
                        server.data.podlings = podlings
                except aiohttp.ClientError:
                    pass
            if plugins.ldap.PROJECTS_OVERRIDE and os.path.exists(plugins.ldap.PROJECTS_OVERRIDE):
                async with ProgTimer("Reading projects override configuration"):
                    try:
                        ldap_override = yaml.safe_load(open(plugins.ldap.PROJECTS_OVERRIDE))
                        for project, data in ldap_override.items():
                            if project not in server.data.pmcs and project != "repository_alternate_owners":
                                print(f"Adding override for virtual project {project}")
                                server.data.pmcs[project] = data.get("owners", [])  # Populate with what's in the file if any.
                    except yaml.YAMLError as err:
                        print(f"Could not load ldap override yaml, {plugins.ldap.PROJECTS_OVERRIDE}: {err}")

            try:
                await adjust_teams(server)
                await adjust_repositories(server)
            except AssertionError as e:
                print("AssertionError happened during GitHub adjustments, bailing for now")
                print(e)
        else:
            print("I could not find any GitHub Teams, not doing setup this round")
        
        async with ProgTimer("Writing github->ldap map file ghmap.yaml"):
            try:
                ghmap = {}
                for person in server.data.people:
                    if person.github_login and person.github_login in server.data.mfa:
                        ghmap[person.github_login] = person.asf_id
                with open("./ghmap.yaml", "w") as f:
                    yaml.dump(ghmap, f)
                    f.close()
            except Exception as e:
                print("Could not write GH map file!")
                print(e)
        
        alimit, aused, aresets = await asf_github_org.rate_limit_graphql()
        used_gql = aused
        if used < aused:
            used_gql -= used
        time_taken = time.time() - now
        print(
            "Background task run finished after %d seconds. Used %d GraphQL tokens for this." % (time_taken, used_gql)
        )
        await asyncio.sleep(server.config.tasks.refresh_rate)
