import { SupabaseClient } from '@supabase/supabase-js';
import { Redis } from 'redis';

import { ISource } from '../models/source.ts';
import { IItem } from '../models/item.ts';
import { IProfile } from '../models/profile.ts';
import { utils } from '../utils/index.ts';
import { feedutils } from './utils/index.ts';

export const getGithubFeed = async (
  supabaseClient: SupabaseClient,
  _redisClient: Redis | undefined,
  profile: IProfile,
  source: ISource,
  _feedData: string | undefined,
): Promise<{ source: ISource; items: IItem[] }> => {
  if (!source.options?.github || !source.options?.github.type) {
    throw new feedutils.FeedValidationError('Invalid source options');
  }

  if (!profile.accountGithub?.token) {
    throw new Error('GitHub token is missing');
  }
  const token = await utils.decrypt(profile.accountGithub.token);

  if (
    source.options.github.type === 'notifications' ||
    source.options.github.type === 'repositorynotifications'
  ) {
    /**
     * With `notifications` and `repositorynotifications` type users can add
     * there GitHub notifications or repository notifications as source to
     * FeedDeck. The notifications are retrieved from the GitHub API via the
     * list notifications endpoint.
     *
     * - https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#list-notifications-for-the-authenticated-user
     * - https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#list-repository-notifications-for-the-authenticated-user
     * - https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#about-notification-reasons
     */
    const notifications = [];

    if (source.options.github.type === 'notifications') {
      const tmpNotifications = await request('/notifications', {
        token: token,
        params: {
          all: 'true',
          participating: source.options.github.participating ? 'true' : 'false',
          page: '1',
          per_page: '50',
        },
      });

      const user = await request('/user', {
        token: token,
      });

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}-${source.options.github.participating}`;
      source.title = user.login;
      source.icon = user.avatar_url;
      source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      notifications.push(...tmpNotifications);
    } else if (
      source.options.github.type === 'repositorynotifications' &&
      source.options.github.repository
    ) {
      const [owner, repo] = source.options.github.repository.split('/');
      const tmpNotifications = await request(
        `/repos/${owner}/${repo}/notifications`,
        {
          token: token,
          params: {
            all: 'true',
            participating: source.options.github.participating
              ? 'true'
              : 'false',
            page: '1',
            per_page: '50',
          },
        },
      );

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}--${source.options.github.participating}-${source.options.github.repository}`;
      source.title = `${owner}/${repo}`;
      if (
        tmpNotifications.length > 0 &&
        tmpNotifications[0].repository?.owner?.avatar_url
      ) {
        source.icon = tmpNotifications[0].repository.owner.avatar_url;
        source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      } else {
        source.icon = `https://github.com/${owner}.png`;
        source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      }
      notifications.push(...tmpNotifications);
    } else {
      throw new feedutils.FeedValidationError('Invalid source options');
    }

    source.type = 'github';
    source.link = 'https://github.com/notifications';

    const items: IItem[] = [];

    for (const [_, notification] of notifications.entries()) {
      items.push({
        id: `${source.id}-${notification.id}`,
        userId: source.userId,
        columnId: source.columnId,
        sourceId: source.id,
        title: notification.subject?.title ?? 'Notification',
        link: getLinkFromApiUrl(notification.subject?.url),
        media: notification.repository.owner.avatar_url,
        description: formatDescription(notification),
        author: notification.repository?.full_name,
        publishedAt: Math.floor(
          new Date(notification.updated_at).getTime() / 1000,
        ),
      });
    }

    return { source, items };
  } else if (
    source.options.github.type === 'useractivities' ||
    source.options.github.type === 'repositoryactivities' ||
    source.options.github.type === 'organizationactivitiespublic' ||
    source.options.github.type === 'organizationactivitiesprivate'
  ) {
    /**
     * `useractivities`, `repositoryactivities`, `organizationactivitiespublic`
     * and `organizationactivitiesprivate` lets a user add the user, repository
     * or organization notifications as source. We are using the corresponding
     * events endpoint to get the notifications and add the id, title, icon and
     * link to the source. Then we are going though all the events and adding
     * the actor of an event as author. The title, description and link is
     * different for each notification type and generated via the `formatEvent`
     * function.
     *
     * - https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types
     * - GitHubTypeUserActivities: https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-events-received-by-the-authenticated-user
     * - GitHubTypeRepositoryActivities: https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-organization-events-for-the-authenticated-user
     * - GitHubTypeOrganizationActivitiesPublic: https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-public-organization-events
     * - GitHubTypeOrganizationActivitiesPrivate: https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-organization-events-for-the-authenticated-user
     */
    const events = [];

    if (
      source.options.github.type === 'useractivities' &&
      source.options.github.user
    ) {
      const tmpEvents = await request(
        `/users/${source.options.github.user}/received_events/public`,
        {
          token: token,
          params: {
            page: '1',
            per_page: '100',
          },
        },
      );

      const user = await request(`/users/${source.options.github.user}`, {
        token: token,
      });

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}-${source.options.github.user}`;
      source.title = source.options.github.user;
      source.icon = user.avatar_url;
      source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      source.link = `https://github.com/${source.options.github.user}`;

      events.push(...tmpEvents);
    } else if (
      source.options.github.type === 'repositoryactivities' &&
      source.options.github.repository
    ) {
      const [owner, repo] = source.options.github.repository.split('/');
      const tmpEvents = await request(
        `/repos/${owner}/${repo}/events`,
        {
          token: token,
          params: {
            page: '1',
            per_page: '100',
          },
        },
      );

      const user = await request(`/users/${owner}`, {
        token: token,
      });

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}-${owner}-${repo}`;
      source.title = `${owner}/${repo}`;
      source.icon = user.avatar_url;
      source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      source.link = `https://github.com/${owner}/${repo}`;

      events.push(...tmpEvents);
    } else if (
      source.options.github.type === 'organizationactivitiespublic' &&
      source.options.github.organization
    ) {
      const tmpEvents = await request(
        `/orgs/${source.options.github.organization}/events`,
        {
          token: token,
          params: {
            page: '1',
            per_page: '100',
          },
        },
      );

      const user = await request(
        `/users/${source.options.github.organization}`,
        {
          token: token,
        },
      );

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}-${source.options.github.organization}`;
      source.title = source.options.github.organization;
      source.icon = user.avatar_url;
      source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      source.link = `https://github.com/${source.options.github.organization}`;

      events.push(...tmpEvents);
    } else if (
      source.options.github.type === 'organizationactivitiesprivate' &&
      source.options.github.organization
    ) {
      const user = await request('/user', {
        token: token,
      });

      const tmpEvents = await request(
        `/users/${user.login}/events/orgs/${source.options.github.organization}`,
        {
          token: token,
          params: {
            page: '1',
            per_page: '100',
          },
        },
      );

      const org = await request(
        `/users/${source.options.github.organization}`,
        {
          token: token,
        },
      );

      source.id =
        `github-${source.userId}-${source.columnId}-${source.options.github.type}-${source.options.github.organization}`;
      source.title = source.options.github.organization;
      source.icon = org.avatar_url;
      source.icon = await feedutils.uploadSourceIcon(supabaseClient, source);
      source.link = `https://github.com/${source.options.github.organization}`;

      events.push(...tmpEvents);
    } else {
      throw new feedutils.FeedValidationError('Invalid source options');
    }

    source.type = 'github';

    const items: IItem[] = [];

    for (const [_, event] of events.entries()) {
      const eventDetails = formatEvent(event);
      if (eventDetails) {
        items.push({
          id: `${source.id}-${event.id}`,
          userId: source.userId,
          columnId: source.columnId,
          sourceId: source.id,
          title: eventDetails.title ?? '',
          link: eventDetails.link ?? '',
          media: event.actor?.avatar_url
            ? event.actor?.avatar_url
            : event.actor?.login
            ? `https://github.com/${event.actor.login}.png`
            : undefined,
          description: eventDetails.description,
          author: event.actor?.login ? event.actor.login : undefined,
          publishedAt: Math.floor(
            new Date(event.created_at).getTime() / 1000,
          ),
        });
      }
    }

    return { source, items };
  } else if (
    source.options.github.type === 'searchissuesandpullrequests' &&
    source.options.github.query
  ) {
    /**
     * The `searchissuesandpullrequests` let a user add a seach query as source.
     * With this type it is possible to follow all issues and pull requests of
     * an user, repository or organization. Since we allow a custom query we do
     * not add a icon and link to the source. The id of the source is generated
     * based on a hash of the query. Then we are going through all the returned
     * issues and generating an item for each issue, where we are using the user
     * as author (we were also thinking about using the repository, but decided
     * against it).
     *
     * - https://docs.github.com/en/rest/search?apiVersion=2022-11-28#search-issues-and-pull-requests
     */
    const issues = await request(
      `/search/issues`,
      {
        token: token,
        params: {
          q: source.options.github.query,
          sort: 'created',
          direction: 'desc',
          page: '1',
          per_page: '100',
        },
      },
    );

    source.id =
      `github-${source.userId}-${source.columnId}-${source.options.github.type}-${await utils
        .md5(source.options.github.query)}`;
    source.type = 'github';
    source.title = source.options.github.queryName || 'Search';
    source.icon = undefined;
    source.link = undefined;

    const items: IItem[] = [];

    for (const [_, issue] of issues.items.entries()) {
      items.push({
        id: `${source.id}-${issue.node_id}`,
        userId: source.userId,
        columnId: source.columnId,
        sourceId: source.id,
        title: issue.title,
        link: issue.html_url,
        media: issue?.user?.avatar_url
          ? issue.user.avatar_url
          : issue?.user?.login
          ? `https://github.com/${issue.user.login}.png`
          : undefined,
        description: `${
          issue.repository_url.replace(
            'https://api.github.com/repos/',
            '',
          )
        } #${issue.number}`,
        author: issue.user.login,
        publishedAt: Math.floor(
          new Date(issue.created_at).getTime() / 1000,
        ),
      });
    }

    return { source, items };
  }

  throw new feedutils.FeedValidationError('Invalid source options');
};

/**
 * `request` is a helper function to make a request to the GitHub API.
 */
const request = async (
  url: string,
  options: { token: string; params?: Record<string, string> },
) => {
  const res = await utils.fetchWithTimeout(
    `https://api.github.com${url}${
      options.params ? `?${new URLSearchParams(options.params).toString()}` : ''
    }`,
    {
      method: 'GET',
      headers: {
        Accept: 'application/vnd.github+json',
        Authorization: `Bearer ${options.token}`,
        'X-GitHub-Api-Version': '2022-11-28',
      },
    },
    5000,
  );

  if (!res.ok) {
    const error = await res.json();
    throw new Error(error?.message ?? 'Unknown error');
  }

  return await res.json();
};

/**
 * `getLinkFromApiUrl` returns a link which can be clicked by a user based on
 * the given GitHub API url. If there is no url given we return the default
 * GitHub notifications link.
 */
const getLinkFromApiUrl = (url?: string): string => {
  if (!url) {
    return 'https://github.com/notifications';
  }

  if (/^https:\/\/api.github.com\/repos\/.*\/.*\/pulls\/\d+$/.test(url)) {
    const n = url.lastIndexOf('pulls');
    url = url.slice(0, n) + url.slice(n).replace('pulls', 'pull');
  }

  return `https://github.com/${
    url.replace('https://api.github.com/repos/', '')
  }`;
};

/**
 * `formatDescription` formats the description for a notification based on the
 * notification reason.
 * See: https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#about-notification-reasons
 */
// deno-lint-ignore no-explicit-any
const formatDescription = (notification: any): string | undefined => {
  switch (notification.reason) {
    case 'assign':
      return 'You were assigned to the issue.';
    case 'author':
      return 'You created the thread.';
    case 'comment':
      return 'You commented on the thread.';
    case 'ci_activity':
      return 'A GitHub Actions workflow run that you triggered was completed.';
    case 'invitation':
      return 'You accepted an invitation to contribute to the repository.';
    case 'manual':
      return 'You subscribed to the thread (via an issue or pull request).';
    case 'mention':
      return 'You were specifically @mentioned in the content.';
    case 'review_requested':
      return "You, or a team you're a member of, were requested to review a pull request.";
    case 'security_alert':
      return 'GitHub discovered a security vulnerability in your repository.';
    case 'state_change':
      return 'You changed the thread state (for example, closing an issue or merging a pull request).';
    case 'subscribed':
      return "You're watching the repository.";
    case 'team_mention':
      return 'You were on a team that was mentioned.';
    default:
      return undefined;
  }
};

/**
 * `formatEvent` formats the given event by returning a proper title, link and
 * description for a item as we save it in our database. If the event type is
 * not supported or a required field is missing we return an error.
 */
const formatEvent = (
  // deno-lint-ignore no-explicit-any
  event: any,
): {
  title: string | undefined;
  link: string | undefined;
  description: string | undefined;
} | undefined => {
  switch (event.type) {
    case 'CommitCommentEvent':
      if (event.payload?.comment?.html_url) {
        return {
          title: '',
          link: event.payload.comment.html_url,
          description: 'Added a comment to a commit',
        };
      }
      return undefined;

    case 'CreateEvent':
      if (event.payload?.ref_type) {
        let description = '';
        switch (event.payload?.ref_type) {
          case 'repository':
            description = 'Created a new repository';
            break;
          case 'branch':
            description = 'Created a new branch';
            break;
          case 'tag':
            description = 'Created a new tag';
            break;
          default:
            description = 'Created something';
            break;
        }
        return {
          title: '',
          link: event.repo.html_url,
          description: description,
        };
      }
      return undefined;

    case 'DeleteEvent':
      if (event.payload?.ref_type) {
        let description = '';
        switch (event.payload?.ref_type) {
          case 'repository':
            description = 'Deleted a repository';
            break;
          case 'branch':
            description = 'Deleted a branch';
            break;
          case 'tag':
            description = 'Deleted a tag';
            break;
          default:
            description = 'Deleted something';
            break;
        }
        return {
          title: '',
          link: event.repo.html_url,
          description: description,
        };
      }
      return undefined;

    case 'ForkEvent':
      if (event.payload?.forkee) {
        return {
          title: event.payload.forkee.name,
          link: event.payload.forkee.html_url,
          description: 'Forked a repository',
        };
      }
      return undefined;

    case 'GollumEvent':
      if (event.payload?.pages && event.payload?.pages.length > 0) {
        let description = '';
        switch (event.payload?.pages[0].action) {
          case 'created':
            description = 'Created a new wiki page';
            break;
          default:
            description = 'Updated a wiki page';
            break;
        }
        return {
          title: event.payload.pages[0].title,
          link: event.payload.pages[0].html_url,
          description: description,
        };
      }
      return undefined;

    case 'IssueCommentEvent':
      if (event.payload?.issue && event.payload?.comment) {
        return {
          title: event.payload.issue.title,
          link: event.payload.comment.html_url,
          description: 'Added a comment',
        };
      }
      return undefined;

    case 'IssuesEvent':
      if (event.payload?.action && event.payload?.issue) {
        let description = '';
        switch (event.payload?.action) {
          case 'assigned':
            description = 'Assigned';
            break;
          case 'unassigned':
            description = 'Unassigned';
            break;
          case 'review_requested':
            description = 'Requested a review';
            break;
          case 'review_request_removed':
            description = 'Removed a requested review';
            break;
          case 'labeled':
            description = 'Added a label';
            break;
          case 'unlabeled':
            description = 'Removed a label';
            break;
          case 'opened':
            description = 'Opened an issue';
            if (event.payload.issue.pull_request) {
              description = 'Opened a pull request';
            }
            break;
          case 'closed':
            description = 'Closed an issue';
            if (event.payload.issue.pull_request) {
              description = 'Closed a pull request';
            }
            break;
          case 'reopened':
            description = 'Reopened an issue';
            if (event.payload.issue.pull_request) {
              description = 'Reopened a pull request';
            }
            break;
          case 'synchronize':
            description = 'Synchronized an issue';
            if (event.payload.issue.pull_request) {
              description = 'Synchronized a pull request';
            }
            break;
          case 'edited':
            description = 'Edited an issue';
            if (event.payload.issue.pull_request) {
              description = 'Edited a pull request';
            }
            break;
        }
        return {
          title: event.payload.issue.title,
          link: event.payload.issue.html_url,
          description: description,
        };
      }
      return undefined;

    case 'MemberEvent':
      if (event.payload?.action && event.payload?.member) {
        let description = '';
        switch (event.payload?.action) {
          case 'added':
            description = 'Was added as member';
            break;
        }
        return {
          title: event.payload.member.login,
          link: event.payload.member.html_url,
          description: description,
        };
      }
      return undefined;

    case 'PullRequestEvent':
      if (event.payload?.action && event.payload?.pull_request) {
        let description = '';
        switch (event.payload?.action) {
          case 'assigned':
            description = 'Assigned';
            break;
          case 'unassigned':
            description = 'Unassigned';
            break;
          case 'review_requested':
            description = 'Requested a review';
            break;
          case 'review_request_removed':
            description = 'Removed a requested review';
            break;
          case 'labeled':
            description = 'Added a label';
            break;
          case 'unlabeled':
            description = 'Removed a label';
            break;
          case 'opened':
            description = 'Opened';
            break;
          case 'closed':
            if (
              event.payload?.pull_request.merged &&
              event.payload?.pull_request.merged == true
            ) {
              description = 'Merged';
              break;
            } else {
              description = 'Closed';
              break;
            }
          case 'reopened':
            description = 'Reopened';
            break;
          case 'synchronize':
            description = 'Synchronized';
            break;
          case 'edited':
            description = 'Edited';
            break;
        }
        return {
          title: event.payload.pull_request.title,
          link: event.payload.pull_request.html_url,
          description: description,
        };
      }
      return undefined;

    case 'PullRequestReviewEvent':
      if (
        event.payload?.pull_request &&
        event.payload?.review
      ) {
        return {
          title: event.payload.pull_request.title,
          link: event.payload.review.html_url,
          description: 'Added a review',
        };
      }
      return undefined;

    case 'PullRequestReviewCommentEvent':
      if (
        event.payload?.action &&
        event.payload?.pull_request &&
        event.payload?.comment
      ) {
        let description = '';
        switch (event.payload?.action) {
          case 'created':
            description = 'Added a review comment';
            break;
          case 'edited':
            description = 'Updated a review comment';
            break;
          case 'deleted':
            description = 'Deleted a review comment';
            break;
        }
        return {
          title: event.payload.pull_request.title,
          link: event.payload.comment.html_url,
          description: description,
        };
      }
      return undefined;

    case 'PushEvent':
      if (event.payload?.repository) {
        return {
          title: '',
          link: event.payload.repository.html_url,
          description: event.payload.commits
            ? event.payload.commits.length === 1
              ? `Pushed ${event.payload.commits.length} commit`
              : `Pushed ${event.payload.commits.length} commits`
            : '',
        };
      }
      return undefined;

    case 'ReleaseEvent':
      if (event.payload?.action && event.payload?.release) {
        let description = '';
        switch (event.payload?.action) {
          case 'created':
            description = 'Release was created';
            break;
          case 'deleted':
            description = 'Release was created';
            break;
          case 'edited':
            description = 'Release was updated';
            break;
          case 'prereleased':
            description = 'Prerelease was created';
            break;
          case 'published':
            description = 'Release was published';
            break;
          case 'released':
            description = 'Release was released';
            break;
          case 'unpublished':
            description = 'Release was unpublished';
            break;
        }
        return {
          title: event.payload.release.name,
          link: event.payload.release.html_url,
          description: description,
        };
      }
      return undefined;

    case 'WatchEvent':
      if (event.actor) {
        return {
          title: '',
          link: `https://github.com/${event.actor.login}`,
          description: `${event.actor.login} starred ${
            event.payload?.repository?.name || 'the repository'
          }`,
        };
      }
      return undefined;

    default:
      return undefined;
  }
};
