import { DateTime } from 'luxon';
import { z } from 'zod';
import { logger } from '../../../logger';
import * as memCache from '../../../util/cache/memory';
import { getCache } from '../../../util/cache/repository';

const GithubIssueBase = z.object({
  number: z.number(),
  state: z.string().transform((val) => val.toLowerCase()),
  title: z.string(),
  body: z.string(),
});

const GithubGraphqlIssue = GithubIssueBase.extend({
  updatedAt: z.string(),
}).transform((issue) => {
  const lastModified = issue.updatedAt;
  const { number, state, title, body } = issue;
  return { number, state, title, body, lastModified };
});

const GithubRestIssue = GithubIssueBase.extend({
  updated_at: z.string(),
}).transform((issue) => {
  const lastModified = issue.updated_at;
  const { number, state, title, body } = issue;
  return { number, state, title, body, lastModified };
});

export const GithubIssue = z.union([GithubGraphqlIssue, GithubRestIssue]);
export type GithubIssue = z.infer<typeof GithubIssue>;

type CacheData = Record<number, GithubIssue>;

export class GithubIssueCache {
  private static reset(cacheData: CacheData | null): void {
    memCache.set('github-issues-reconcile-queue', null);
    const repoCache = getCache();
    repoCache.platform ??= {};
    repoCache.platform.github ??= {};
    if (cacheData) {
      repoCache.platform.github.issuesCache = cacheData;
    } else {
      delete repoCache.platform.github.issuesCache;
    }
  }

  private static get data(): CacheData | null {
    let cacheData: CacheData | undefined | null = getCache().platform?.github
      ?.issuesCache as CacheData | undefined;
    if (!cacheData) {
      return null;
    }

    cacheData = this.reconcile(cacheData);
    return cacheData;
  }

  static getIssues(): GithubIssue[] | null {
    const cacheData = this.data;
    if (!cacheData) {
      return null;
    }

    const sortedResult = Object.values(cacheData).sort(
      ({ lastModified: a }, { lastModified: b }) =>
        DateTime.fromISO(b).toMillis() - DateTime.fromISO(a).toMillis(),
    );

    return sortedResult;
  }

  static setIssues(issues: GithubIssue[]): void {
    const cacheData: CacheData = {};
    for (const issue of issues) {
      cacheData[issue.number] = issue;
    }

    logger.debug(`Issues cache: Setting ${issues.length} issues in cache`);
    this.reset(cacheData);
  }

  static updateIssue(issue: GithubIssue): void {
    const cacheData = this.data;
    if (cacheData) {
      logger.debug(`Issues cache: Updating issue ${issue.number} in cache`);
      cacheData[issue.number] = issue;
    }
  }

  static deleteIssue(number: number): void {
    const cacheData = this.data;
    if (cacheData) {
      logger.debug(`Issues cache: Deleting issue ${number} from cache`);
      delete cacheData[number];
    }
  }

  /**
   * At the moment of repo initialization, repository cache is not available.
   * What we can do is to store issues for later reconciliation.
   */
  static addIssuesToReconcile(issues: GithubIssue[] | undefined): void {
    logger.debug(
      `Issues cache: Adding ${issues?.length} issues to reconcile queue`,
    );
    memCache.set('github-issues-reconcile-queue', issues);
  }

  private static reconcile(cacheData: CacheData): CacheData | null {
    const issuesToReconcile = memCache.get<GithubIssue[]>(
      'github-issues-reconcile-queue',
    );
    if (!issuesToReconcile) {
      return cacheData;
    }

    let isReconciled = false;

    for (const issue of issuesToReconcile) {
      const cachedIssue = cacheData[issue.number];

      // If we reached the item which is already in the cache,
      // it means sync is done.
      if (
        cachedIssue &&
        cachedIssue.number === issue.number &&
        cachedIssue.lastModified === issue.lastModified
      ) {
        isReconciled = true;
        logger.debug(`Issues cache: Done reconciling at issue ${issue.number}`);
        break;
      }

      cacheData[issue.number] = issue;
    }

    // If we've just iterated over all the items in the cache,
    // it means sync is also done.
    if (issuesToReconcile.length >= Object.keys(cacheData).length) {
      logger.debug(
        `Issues cache: Done reconciling by iterating over all items`,
      );
      isReconciled = true;
    }

    if (!isReconciled) {
      logger.debug('Issues cache: reset');
      this.reset(null);
      return null;
    }

    logger.debug('Issues cache: synced');
    this.reset(cacheData);
    return cacheData;
  }
}
