/*
 * Copyright (C) 2018-2022 Open StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {
  Api,
  AccessLevel,
  IssueState,
  MembershipScope,
  MergeRequestState,
  Milestone,
  Project,
  Scope,
} from '@openstapps/gitlab-api';
import {Logger} from '@openstapps/logger';
import {getProjects} from '../common.js';
import {
  CONCURRENCY,
  GROUPS,
  NEEDED_LABELS,
  NEEDED_MILESTONES,
  NOTE_PREFIX,
  PROTECTED_BRANCHES,
  SCHOOLS,
} from '../configuration.js';
import {mapAsyncLimit} from '@openstapps/collection-utils';

/**
 * Tidy issues without a milestone
 *
 * This will set the milestone of issues without a milestone to 'Meeting'.
 * @param api GitLab API instance for the requests
 */
export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
  // fetch issues without a milestone from all groups
  const issuesWithoutMilestone = await mapAsyncLimit(
    GROUPS,
    groupId =>
      api.getIssues({
        groupId: groupId,
        milestone: 'No Milestone',
        state: IssueState.OPENED,
      }),
    CONCURRENCY,
  ).then(it => it.flat());

  Logger.info(`Found '${issuesWithoutMilestone.length}' issue(s) without milestone.`);

  const milestoneCache: {[s: number]: Milestone[]} = {};

  await mapAsyncLimit(
    issuesWithoutMilestone,
    async issue => {
      if (milestoneCache[issue.project_id] === undefined) {
        milestoneCache[issue.project_id] = await api.getMilestonesForProject(issue.project_id);
      }

      let milestoneId;

      for (const milestone of milestoneCache[issue.project_id]) {
        if (milestone.title === 'Meeting') {
          milestoneId = milestone.id;
        }
      }

      if (milestoneId === undefined) {
        Logger.warn(`Milestone 'Meeting' was not available for issue ${issue.title} (${issue.web_url}).`);

        return;
      }

      await api.setMilestoneForIssue(issue, milestoneId);

      Logger.log(`Milestone was set to 'Meeting' for issue ${issue.title} (${issue.web_url})`);

      await api.createNote(
        issue.project_id,
        Scope.ISSUES,
        issue.iid,
        `${NOTE_PREFIX} Milestone was set automatically to 'Meeting'.`,
      );
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied issues without milestones.');
}

/**
 * Tidy open issues without meeting label
 *
 * This adds the label 'meeting' to all open issues that do not have this label.
 * @param api GitLab API instance for requests
 */
export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void> {
  // fetch all open issues
  const openIssues = await mapAsyncLimit(
    GROUPS,
    groupId =>
      api.getIssues({
        groupId: groupId,
        state: IssueState.OPENED,
      }),
    CONCURRENCY,
  ).then(it => it.flat());

  Logger.info(`Found ${openIssues.length} open issue(s).`);

  // filter issues without meeting label
  const openIssuesWithoutMeetingLabel = openIssues.filter(openIssue => {
    return !openIssue.labels.includes('meeting');
  });

  Logger.info(`Filtered ${openIssuesWithoutMeetingLabel.length} open issue(s) without label 'meeting'.`);

  await mapAsyncLimit(
    openIssuesWithoutMeetingLabel,
    async issue => {
      if (issue.milestone !== null && issue.milestone.title === 'Backlog') {
        Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`);

        return;
      }

      return api.createNote(
        issue.project_id,
        Scope.ISSUES,
        issue.iid,
        `${NOTE_PREFIX} Automatically adding label 'meeting'\n\n/label ~meeting`,
      );
    },
    CONCURRENCY,
  );

  Logger.ok(`Tidied open issues without label 'meeting'.`);
}

/**
 * Tidy labels in a list of projects
 * @param api GitLab API instance to use for the requests
 * @param projects List of projects to tidy labels on
 */
export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
  await mapAsyncLimit(
    projects,
    async project => {
      const labels = await api.getLabels(project.id);

      const neededLabels = [...NEEDED_LABELS];
      // const extraneousLabels: Label[] = [];

      for (const label of labels) {
        // let needed = false;

        for (const [neededLabelIndex, neededLabel] of neededLabels.entries()) {
          if (neededLabel.name.toLowerCase() === label.name.toLowerCase()) {
            neededLabels.splice(neededLabelIndex, 1);
            // needed = true;
          }
        }

        /* if (!needed) {
        extraneousLabels.push(label);
      } */
      }

      await mapAsyncLimit(
        neededLabels,
        async neededLabel => {
          await api.createLabel(project.id, neededLabel.name, neededLabel.description, neededLabel.color);

          Logger.log(`Created label '${neededLabel.name}' in '${project.name_with_namespace}'.`);
        },
        CONCURRENCY,
      );

      // await asyncPool(2, extraneousLabels, async (extraneousLabel) => {
      //   await api.deleteLabel(project.id, extraneousLabel.name);
      //
      //   Logger.log('Deleted label `' + extraneousLabel.name + '` from ' + project.name_with_namespace + '.');
      // });
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied labels.');
}

/**
 * Tidy milestones in a list of projects
 * @param api GitLab API instance to use for the requests
 * @param projects List of projects to tidy milestones on
 */
export async function tidyMilestones(api: Api, projects: Project[]): Promise<void> {
  await mapAsyncLimit(
    projects,
    async project => {
      const milestones = await api.getMilestonesForProject(project.id);
      const missingMilestones = [...NEEDED_MILESTONES];

      for (const milestone of milestones) {
        const index = missingMilestones.indexOf(milestone.title);

        if (index >= 0) {
          missingMilestones.splice(index, 1);
        }
      }

      if (missingMilestones.length > 0 && !project.archived) {
        await Promise.all(
          missingMilestones.map(async milestone => {
            await api.createMilestone(project.id, milestone);
            Logger.log(`Created milestone '${milestone}' for project ${project.name_with_namespace}'.`);
          }),
        );
      }
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied milestones.');
}

/**
 * Tidy protected branches in a list of projects
 * @param api GitLab API instance to use for the requests
 * @param projects List of projects to tidy milestones on
 */
export async function tidyProtectedBranches(api: Api, projects: Project[]): Promise<void> {
  await mapAsyncLimit(
    projects,
    async project => {
      const branches = await api.getBranchesForProject(project.id);

      const protectableBranches = branches.filter(branch => {
        return PROTECTED_BRANCHES.includes(branch.name);
      });

      const unprotectedBranches = protectableBranches.filter(branch => {
        return !branch.protected;
      });

      await Promise.all(
        unprotectedBranches.map(async branch => {
          await api.protectBranch(project.id, branch.name);

          Logger.log(
            `Added protected branch '${branch.name}' in project '${project.name_with_namespace}'...`,
          );
        }),
      );
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied protected branches.');
}

/**
 * Tidy protected tags
 * @param api GitLab API instance to use for the requests
 * @param projects List of projects to tidy protected tags on
 */
export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<void> {
  await mapAsyncLimit(
    projects,
    async project => {
      // TODO: move this to GitLab API
      const protectedTags = (await api.makeGitLabAPIRequest(
        `projects/${project.id}/protected_tags`,
      )) as Array<{
        /**
         * List of access levels to create a tag
         */
        create_access_levels: Array<{
          /**
           * Access level
           */
          access_level: AccessLevel;
          /**
           * Description of access level
           */
          access_level_description: string;
        }>;
        /**
         * Name of the tag
         */
        name: string;
      }>;

      if (
        protectedTags.findIndex(protectedTag => {
          return protectedTag.name === 'v*';
        }) === -1
      ) {
        await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, {
          data: {
            create_access_level: AccessLevel.Maintainer,
            name: 'v*',
          },
          method: 'POST',
        });

        Logger.log(`Added protected version tag in project '${project.name_with_namespace}'.`);
      }
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied protected tags.');
}

/**
 * Tidy "sub" group members
 * @param api GitLab API instance to use for the requests
 */
export async function tidySubGroupMembers(api: Api): Promise<void> {
  const stappsMembers = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]);
  const stappsMemberIds = new Set(stappsMembers.map(member => member.id));

  const groupIdsToSchool: {[id: number]: string} = {};

  Object.keys(SCHOOLS).map(school => {
    groupIdsToSchool[SCHOOLS[school]] = school;
  });

  await mapAsyncLimit(
    GROUPS.slice(1),
    async groupId => {
      const members = await api.getMembers(MembershipScope.GROUPS, groupId);
      const memberIds = new Set(members.map(member => member.id));

      await mapAsyncLimit(
        stappsMembers,
        async stappsMember => {
          if (!memberIds.has(stappsMember.id)) {
            await api.addMember(MembershipScope.GROUPS, groupId, stappsMember.id, AccessLevel.Developer);

            Logger.log(`Added '${stappsMember.name}' to group '${groupIdsToSchool[groupId]}'.`);
          }
        },
        CONCURRENCY,
      );

      await mapAsyncLimit(
        members,
        async member => {
          if (!stappsMemberIds.has(member.id)) {
            await api.deleteMember(MembershipScope.GROUPS, groupId, member.id);

            Logger.log(`Deleted member '${member.name}' from group '${groupIdsToSchool[groupId]}'.`);
          }
        },
        CONCURRENCY,
      );
    },
    CONCURRENCY,
  );

  Logger.ok(`Tidied 'sub' group members.`);
}

/**
 * Tidy issues without assignee
 *
 * Set assignee to author if no assignee is set.
 * @param api GitLab API instance for the requests
 */
export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
  // fetch issues without a milestone from all groups
  const issues = await mapAsyncLimit(
    GROUPS,
    async groupId => {
      return api.getIssues({
        groupId: groupId,
        state: IssueState.OPENED,
      });
    },
    CONCURRENCY,
  ).then(it => it.flat());

  const issuesWithoutAssignee = issues.filter(issue => {
    return issue.assignee === null;
  });

  Logger.info(`Found '${issuesWithoutAssignee.length}' issue(s) without assignee.`);

  await mapAsyncLimit(
    issuesWithoutAssignee,
    async issue => {
      await api.setAssigneeForIssue(issue, issue.author.id);

      Logger.log(`Set assignee for '${issue.title}' to '${issue.author.name}'.`);

      await api.createNote(
        issue.project_id,
        Scope.ISSUES,
        issue.iid,
        `${NOTE_PREFIX} Assignee was set automatically to author.`,
      );
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied issues without assignee.');
}

/**
 * Tidy merge requests without assignee
 *
 * Set assignee to author if no assignee is set.
 * @param api GitLab API instance for the requests
 */
export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void> {
  const mergeRequests = await mapAsyncLimit(
    GROUPS,
    async groupId => {
      return api.getMergeRequests(MembershipScope.GROUPS, groupId, MergeRequestState.OPENED);
    },
    CONCURRENCY,
  ).then(it => it.flat());

  const mergeRequestsWithoutAssignee = mergeRequests.filter(mergeRequest => {
    return mergeRequest.assignee === null;
  });

  Logger.info(`Found '${mergeRequestsWithoutAssignee.length}' merge requests without assignee.`);

  await mapAsyncLimit(
    mergeRequestsWithoutAssignee,
    async mergeRequest => {
      await api.setAssigneeForMergeRequest(mergeRequest, mergeRequest.author.id);

      Logger.log(`Set assignee for '${mergeRequest.title}' to '${mergeRequest.author.name}'.`);

      await api.createNote(
        mergeRequest.project_id,
        Scope.MERGE_REQUESTS,
        mergeRequest.iid,
        `${NOTE_PREFIX} Assignee was set automatically to author.`,
      );
    },
    CONCURRENCY,
  );

  Logger.ok('Tidied merge requests without assignee.');
}

/**
 * Tidy
 * @param api GitLab API instance to use for the requests
 */
export async function tidy(api: Api) {
  // get first level subgroups
  const groups = [...GROUPS];
  const subGroups = await api.getSubGroupsForGroup(groups[0]);
  groups.push(...groups, ...subGroups.map(group => group.id));

  // get non archived projects of groups
  let projects = await getProjects(api, groups);
  projects = projects.filter(project => !project.archived);

  await Promise.all([
    // Labels are now specified at Group level
    // await tidyLabels(api, projects),
    await tidyMilestones(api, projects),
    await tidyProtectedBranches(api, projects),
    await tidyProtectedTags(api, projects),
  ]);

  await tidyOpenIssuesWithoutMeetingLabel(api);
  await tidyIssuesWithoutAssignee(api);
  await tidyMergeRequestsWithoutAssignee(api);
}
