import {findModuleExport, Patch} from "@decky/ui";
import {replacePatch} from "decky-frontend-lib";
import remarkHtml from "remark-html"
import remarkParse from "remark-parse"
import remarkGfm from "remark-gfm"
import {unified} from "unified"
import html2bbcode from "./html2bbcode";
import {Mutex} from 'async-mutex';

const PartnerEventStore = findModuleExport(
  (e) => e?.prototype?.InternalLoadAdjacentPartnerEvents
);

const SteamID = findModuleExport(
  (e) => e?.prototype?.BIsClanAccount
    && e?.prototype?.BIsIndividualAccount
    && e?.prototype?.BIsValid
    && e?.prototype?.ConvertTo64BitString
    && e?.prototype?.GetAccountID
    && e?.prototype?.GetAccountType
    && e?.prototype?.GetInstance
    && e?.prototype?.GetUniverse
    && e?.prototype?.Render
    && e?.prototype?.SetAccountID
    && e?.prototype?.SetAccountType
    && e?.prototype?.SetFromComponents
    && e?.prototype?.SetInstance
    && e?.prototype?.SetUniverse
);

const steamClanSteamID = "103582791470414830";
const steamClanID = "40893422";
const steamOSAppId = 1675200;
const githubReleasesURI = "https://api.github.com/repos/ublue-os/bazzite/releases";
const generator = fetchReleases();

enum SteamEventType {
  SmallUpdate = 12,
  Update = 13,
  BigUpdate = 14,
}

type SteamTags = {
  require_tags?: string[]
}

enum SteamOSChannel {
  Stable = "stablechannel",
  Beta = "betachannel",
  Preview = "previewchannel",
}

const mutex = new Mutex();
const cachedGithubReleases: { gid: string, release: any }[] = [];

export function patchPartnerEventStore(): Patch[] {
  const loadAdjacentPartnerEventsPatch = replacePatch(
    PartnerEventStore.prototype,
    "LoadAdjacentPartnerEvents",
    async function (args) {
      let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args;
      const module = this;

      if (appId !== steamOSAppId) {
        return module.InternalLoadAdjacentPartnerEvents(gidEvent, void 0, gidAnnouncement, appId, countBefore, countAfter, tags, token);
      }

      return mutex.runExclusive(async () => {
        return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter);
      });
    }
  );

  const loadAdjacentPartnerEventsByAnnouncementPatch = replacePatch(
    PartnerEventStore.prototype,
    "LoadAdjacentPartnerEventsByAnnouncement",
    async function (args) {
      let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args;
      const module = this;

      if (appId !== steamOSAppId) {
        return module.InternalLoadAdjacentPartnerEvents(void 0, gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token);
      }

      return mutex.runExclusive(async () => {
        return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter);
      });
    }
  );

  const loadAdjacentPartnerEventsByEventPatch = replacePatch(
    PartnerEventStore.prototype,
    "LoadAdjacentPartnerEventsByEvent",
    async function (args) {
      let [gidEvent, gidAnnouncement, appId, countBefore, countAfter, tags, token] = args;
      const module = this;

      if (appId !== steamOSAppId) {
        const clanId = gidAnnouncement || gidEvent.clanSteamID;

        return gidEvent.bOldAnnouncement
          ? module.InternalLoadAdjacentPartnerEvents(void 0, gidEvent.AnnouncementGID, clanId, appId, countBefore, countAfter, tags, token)
          : module.InternalLoadAdjacentPartnerEvents(gidEvent.GID, gidEvent.AnnouncementGID, clanId, appId, countBefore, countAfter, tags, token);
      }

      return mutex.runExclusive(async () => {
        return LoadBazziteReleasesAsPartnerEvents(module, gidEvent?.GID || gidEvent?.AnnouncementGID, tags, countBefore, countAfter);
      });
    }
  );

  return [
    loadAdjacentPartnerEventsPatch,
    loadAdjacentPartnerEventsByAnnouncementPatch,
    loadAdjacentPartnerEventsByEventPatch
  ]
}

async function LoadBazziteReleasesAsPartnerEvents(module: any, gid: any, tags: SteamTags, countBefore: number, countAfter: number) {
  const ret: any[] = [];

  // InternalLoadAdjacentPartnerEvents minified code, gets announcement from cache if it exists
  if (module.m_mapAdjacentAnnouncementGIDs.has(gid)) {
    // noinspection JSPrimitiveTypeWrapperUsage
    let e = module.m_mapAdjacentAnnouncementGIDs.get(gid)
      , r = new Array;
    // noinspection CommaExpressionJS
    if (e.forEach(((e: any) => {
        if (module.m_mapAnnouncementBodyToEvent.has(e)) {
          let t = module.m_mapAnnouncementBodyToEvent.get(e);
          ret.push(module.m_mapExistingEvents.get(t))
        } else
          r.push(e)
      }
    )),
    r.length > 0) {
      (await module.LoadBatchPartnerEventsByEventGIDsOrAnnouncementGIDs(null, r, tags)).forEach(((e: any) => ret.push(e)))
    }
  }

  if (cachedGithubReleases.length === 0) {
    await fetchMoreReleases(countAfter, isBetaOrPreviewChannel(tags));
  }

  const releaseIndex = gid ? cachedGithubReleases.findIndex((e: any) => e.gid === gid) : -1;
  let releases: { gid: any, release: any }[];

  if (releaseIndex === -1) {
    releases = cachedGithubReleases.slice(0, countAfter);
  } else {
    if (releaseIndex + countAfter + 1 > cachedGithubReleases.length) {
      const toFetch = releaseIndex + countAfter + 1 - cachedGithubReleases.length;
      await fetchMoreReleases(toFetch, isBetaOrPreviewChannel(tags));
    }

    releases = cachedGithubReleases.slice(Math.max(releaseIndex - countBefore + 1, 0), releaseIndex + countAfter + 1);
  }

  for (const {release} of releases) {
    const releasePublishedAt = Math.floor((new Date(release.published_at)).getTime() / 1000);

    const html = await unified()
      .use(remarkParse)
      .use(remarkGfm)
      .use(remarkHtml)
      .process(release.body);

    const converter = new (html2bbcode.HTML2BBCode)();
    const bbcode = converter.feed(html.value);

    // haven't found a way to hide likes and comments yet
    const event = {
      "gid": String(release.id),
      "clan_steamid": steamClanSteamID,
      "event_name": release.name,
      "event_type": SteamEventType.Update,
      "appid": steamOSAppId,
      "server_address": "",
      "server_password": "",
      // start and end time are only used for certain event_type, not used for updates, but fill it anyway
      "rtime32_start_time": releasePublishedAt,
      "rtime32_end_time": releasePublishedAt,
      "comment_count": 0,
      "creator_steamid": "0",
      "last_update_steamid": "0",
      "event_notes": "see announcement body",
      "jsondata": "",
      "announcement_body": {
        "gid": String(release.id),
        "clanid": steamClanID,
        "posterid": "0",
        "headline": `Bazzite ${release.tag_name}`,
        "posttime": releasePublishedAt,
        "updatetime": releasePublishedAt,
        "body": bbcode.toString(),
        "commentcount": 0,
        "tags": [
          "patchnotes",
          isBetaOrPreviewChannel(tags) ? SteamOSChannel.Beta : SteamOSChannel.Stable,
        ],
        "language": 0,
        "hidden": 0,
        "forum_topic_id": "0",
        "event_gid": "0",
        "voteupcount": 0,
        "votedowncount": 0,
        "ban_check_result": 0,
        "banned": 0
      },
      "published": 1,
      "hidden": 0,
      "rtime32_visibility_start": 0,
      "rtime32_visibility_end": 0,
      "broadcaster_accountid": 0,
      "follower_count": 0,
      "ignore_count": 0,
      "forum_topic_id": "0",
      "rtime32_last_modified": releasePublishedAt,
      "news_post_gid": "0",
      "rtime_mod_reviewed": 0,
      "featured_app_tagid": 0,
      "referenced_appids": [],
      "build_id": 0,
      "build_branch": "",
      "unlisted": 0,
      "votes_up": 0,
      "votes_down": 0,
      "comment_type": "ForumTopic",
      "gidfeature": "0",
      "gidfeature2": "0"
    };

    // InternalLoadAdjacentPartnerEvents minified code, maps announcement and add it to cache
    if (!module.m_mapExistingEvents.has(event.gid)) {
      let steamId = new SteamID(event.clan_steamid);
      module.InsertEventModelFromClanEventData(steamId, event)
    }

    ret.push(module.m_mapExistingEvents.get(event.gid));
  }

  return ret;
}

async function fetchMoreReleases(count: number, beta: boolean) {
  const releases = [];
  let iterator;

  do {
    iterator = await generator.next();
    const release = iterator.value;

    if ((beta && release.prerelease) || (!beta && !release.prerelease))
      releases.push(release);
  } while (releases.length < count && !iterator.done)

  for (const release of releases) {
    cachedGithubReleases.push({gid: String(release.id), release});
  }
}

async function* fetchReleases() {
  let currentPage = 1;
  let done = false;

  while (!done) {
    let response: Response;
    let responseJson: any;

    try {
      response = await fetch(githubReleasesURI + `?page=${currentPage++}&per_page=10`);

      if (response.ok) {
        responseJson = await response.json();
      } else {
        responseJson = [];
      }
    } catch {
      responseJson = [];
    }

    if (!Array.isArray(responseJson) || responseJson.length == 0) {
      done = true;
    } else {
      responseJson.sort((a, b) => (new Date(b.created_at)).getTime() - (new Date(a.created_at)).getTime());

      for (let release of responseJson) {
        yield release;
      }
    }
  }

  return undefined;
}

function isBetaOrPreviewChannel(tags: SteamTags): boolean {
  return (tags?.require_tags
      && (tags?.require_tags?.includes(SteamOSChannel.Beta)
        || tags?.require_tags?.includes(SteamOSChannel.Preview)))
    ?? false;
}
