import teamsnapScriptUrl from "teamsnap.js/lib/teamsnap.js?url";

import { api } from "../api/client";
import type {
  TeamSnapAssignment,
  TeamSnapAvailability,
  TeamSnapEvent,
  TeamSnapEventLineup,
  TeamSnapEventLineupEntry,
  TeamSnapMember,
  TeamSnapTeam,
  TeamSnapUser,
} from "../api/types";
import { staleWhileRevalidateTeamSnap, teamSnapQueryKeys } from "./teamSnapCache";

export { teamSnapQueryKeys } from "./teamSnapCache";

type TeamSnapSdk = {
  auth?: (token: string) => Promise<void> | void;
  enablePersistence?: () => void;
  loadCollections?: () => Promise<void>;
  loadMe?: () => Promise<TeamSnapUser>;
  loadTeams?: (...args: unknown[]) => Promise<TeamSnapTeam[]>;
  loadMembers?: (params: unknown) => Promise<TeamSnapMember[]>;
  loadEvents?: (params: unknown) => Promise<TeamSnapEvent[]>;
  loadEventLineups?: (params: unknown) => Promise<TeamSnapEventLineup[]>;
  loadAvailabilities?: (params: unknown) => Promise<TeamSnapAvailability[]>;
  loadAssignments?: (params: unknown) => Promise<TeamSnapAssignment[]>;
  bulkLoad?: (teamIdOrParams: string | number | Record<string, unknown>, typesOrParams?: unknown) => Promise<TeamSnapBulkItem[]>;
  createEventLineup?: (data?: Record<string, unknown>) => unknown;
  saveEventLineup?: (eventLineup: unknown) => Promise<unknown> | void;
  deleteEventLineup?: (eventLineup: unknown) => Promise<unknown> | void;
  createEventLineupEntry?: (data?: Record<string, unknown>) => unknown;
  saveEventLineupEntry?: (eventLineupEntry: unknown) => Promise<unknown> | void;
  deleteEventLineupEntry?: (eventLineupEntry: unknown) => Promise<unknown> | void;
  memberName?: (member: TeamSnapMember, reverse?: boolean, forSort?: boolean) => string;
  collections?: {
  };
};

type TeamSnapBulkItem = {
  type?: string;
  id?: number | string;
  eventId?: number | string;
  eventLineupId?: number | string;
  memberId?: number | string;
  sequence?: number | string | null;
  [key: string]: unknown;
};

function toId(value: number | string | undefined | null): string {
  return value == null ? "" : String(value);
}

function toSequence(value: number | string | null | undefined): number {
  if (value == null || value === "") {
    return Number.MAX_SAFE_INTEGER;
  }

  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
}

function isEventLineup(item: unknown): item is TeamSnapEventLineup {
  return Boolean(item) && typeof item === "object" && (item as TeamSnapBulkItem).type === "eventLineup";
}

function isEventLineupEntry(item: unknown): item is TeamSnapEventLineupEntry {
  return Boolean(item) && typeof item === "object" && (item as TeamSnapBulkItem).type === "eventLineupEntry";
}

function sortLineupEntries(entries: TeamSnapEventLineupEntry[]): TeamSnapEventLineupEntry[] {
  return entries.slice().sort((left, right) => {
    const leftSequence = toSequence(left.sequence);
    const rightSequence = toSequence(right.sequence);
    if (leftSequence !== rightSequence) {
      return leftSequence - rightSequence;
    }
    return toId(left.id).localeCompare(toId(right.id));
  });
}

function normalizeBulkLineupData(
  items: TeamSnapBulkItem[],
  eventId: string,
): {
  eventLineup: TeamSnapEventLineup | null;
  entries: TeamSnapEventLineupEntry[];
} {
  const eventLineups = items.filter(isEventLineup).filter((item) => toId(item.eventId) === eventId);
  const eventLineup = eventLineups.length ? eventLineups[eventLineups.length - 1] : null;
  const lineupId = toId(eventLineup?.id);
  const entries = items
    .filter(isEventLineupEntry)
    .filter((item) => !lineupId || toId(item.eventLineupId) === lineupId);

  return {
    eventLineup,
    entries: sortLineupEntries(entries),
  };
}

let sdkPromise: Promise<TeamSnapSdk> | null = null;

declare global {
  interface Window {
    teamsnap?: TeamSnapSdk;
  }
}

function loadScript(src: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const existing = document.querySelector<HTMLScriptElement>(`script[data-sdk="teamsnap"][src="${src}"]`);
    if (existing) {
      if (window.teamsnap) {
        resolve();
        return;
      }
      existing.addEventListener("load", () => resolve(), { once: true });
      existing.addEventListener("error", () => reject(new Error("Failed to load TeamSnap SDK")), { once: true });
      return;
    }

    const script = document.createElement("script");
    script.src = src;
    script.async = true;
    script.dataset.sdk = "teamsnap";
    script.onload = () => resolve();
    script.onerror = () => reject(new Error("Failed to load TeamSnap SDK"));
    document.head.appendChild(script);
  });
}

async function getSdk(): Promise<TeamSnapSdk> {
  if (!sdkPromise) {
    sdkPromise = loadScript(teamsnapScriptUrl).then(() => {
      if (!window.teamsnap) {
        throw new Error("TeamSnap SDK did not initialize");
      }
      return window.teamsnap;
    });
  }
  return sdkPromise;
}

async function ensureAuthorized(): Promise<TeamSnapSdk> {
  const sdk = await getSdk();
  const token = await api.getTeamSnapToken();
  (sdk as TeamSnapSdk & { apiUrl?: string; authUrl?: string }).apiUrl = token.api_root;
  (sdk as TeamSnapSdk & { apiUrl?: string; authUrl?: string }).authUrl = token.auth_url;
  if (sdk.auth) {
    await sdk.auth(token.access_token);
  }
  if (sdk.loadCollections) {
    await sdk.loadCollections();
  }
  if (sdk.enablePersistence) {
    sdk.enablePersistence();
  }
  return sdk;
}

export const teamsnapClient = {
  async loadMe(cacheScope?: string | number | null): Promise<TeamSnapUser | null> {
    return staleWhileRevalidateTeamSnap<TeamSnapUser | null>({
      cacheParts: teamSnapQueryKeys.me(cacheScope),
      queryKey: teamSnapQueryKeys.me(cacheScope),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadMe) {
          return sdk.loadMe();
        }
        return null;
      },
    });
  },
  async loadTeams(cacheScope?: string | number | null): Promise<TeamSnapTeam[]> {
    return staleWhileRevalidateTeamSnap<TeamSnapTeam[]>({
      cacheParts: teamSnapQueryKeys.teams(cacheScope),
      queryKey: teamSnapQueryKeys.teams(cacheScope),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadTeams) {
          const teams = await sdk.loadTeams();
          return teams.filter((team) => team.isRetired !== true);
        }
        return [];
      },
    });
  },
  async loadMembers(teamId: string, cacheScope?: string | number | null): Promise<TeamSnapMember[]> {
    return staleWhileRevalidateTeamSnap<TeamSnapMember[]>({
      cacheParts: teamSnapQueryKeys.members(cacheScope, teamId),
      queryKey: teamSnapQueryKeys.members(cacheScope, teamId),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadMembers) {
          return sdk.loadMembers({ teamId });
        }
        return [];
      },
    });
  },
  async loadEvents(teamId: string, cacheScope?: string | number | null): Promise<TeamSnapEvent[]> {
    return staleWhileRevalidateTeamSnap<TeamSnapEvent[]>({
      cacheParts: teamSnapQueryKeys.events(cacheScope, teamId),
      queryKey: teamSnapQueryKeys.events(cacheScope, teamId),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadEvents) {
          return sdk.loadEvents({ teamId });
        }
        return [];
      },
    });
  },
  async loadAvailabilities(teamId: string, eventId?: string, cacheScope?: string | number | null): Promise<TeamSnapAvailability[]> {
    return staleWhileRevalidateTeamSnap<TeamSnapAvailability[]>({
      cacheParts: teamSnapQueryKeys.availabilities(cacheScope, teamId, eventId),
      queryKey: teamSnapQueryKeys.availabilities(cacheScope, teamId, eventId),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadAvailabilities) {
          return sdk.loadAvailabilities(eventId ? { teamId, eventId } : { teamId });
        }
        return [];
      },
    });
  },
  async loadAssignments(teamId: string, eventId?: string, cacheScope?: string | number | null): Promise<TeamSnapAssignment[]> {
    return staleWhileRevalidateTeamSnap<TeamSnapAssignment[]>({
      cacheParts: teamSnapQueryKeys.assignments(cacheScope, teamId, eventId),
      queryKey: teamSnapQueryKeys.assignments(cacheScope, teamId, eventId),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();
        if (sdk.loadAssignments) {
          return sdk.loadAssignments(eventId ? { teamId, eventId } : { teamId });
        }
        return [];
      },
    });
  },
  async loadEventLineupData(teamId: string, eventId: string, cacheScope?: string | number | null): Promise<{
    eventLineup: TeamSnapEventLineup | null;
    entries: TeamSnapEventLineupEntry[];
  }> {
    return staleWhileRevalidateTeamSnap<{
      eventLineup: TeamSnapEventLineup | null;
      entries: TeamSnapEventLineupEntry[];
    }>({
      cacheParts: teamSnapQueryKeys.eventLineup(cacheScope, teamId, eventId),
      queryKey: teamSnapQueryKeys.eventLineup(cacheScope, teamId, eventId),
      fetchFresh: async () => {
        const sdk = await ensureAuthorized();

        if (sdk.bulkLoad) {
          try {
            const bulkItems = await sdk.bulkLoad({
              teamId,
              types: ["eventLineup", "eventLineupEntry"],
              scopeTo: "event",
              event__id: eventId,
            });
            const lineupData = normalizeBulkLineupData(bulkItems, eventId);
            if (lineupData.eventLineup || lineupData.entries.length) {
              return lineupData;
            }
          } catch {
            // Fall back to rel-driven reads when bulk load does not return lineup data.
          }
        }

        if (!sdk.loadEventLineups) {
          return { eventLineup: null, entries: [] };
        }

        const eventLineups = await sdk.loadEventLineups({ eventId });
        const matchingEventLineups = eventLineups.filter((item) => toId(item.eventId) === eventId);
        const eventLineup = matchingEventLineups.length ? matchingEventLineups[matchingEventLineups.length - 1] : null;

        const eventLineupWithLinks = eventLineup as TeamSnapEventLineup & {
          loadItems?: (linkName: string) => Promise<TeamSnapEventLineupEntry[]>;
        } | null;
        if (!eventLineupWithLinks?.loadItems) {
          return { eventLineup, entries: [] };
        }

        try {
          const rawEntries = await eventLineupWithLinks.loadItems("eventLineupEntries");
          const lineupId = toId(eventLineupWithLinks.id);
          const entries = sortLineupEntries(
            rawEntries
              .filter(isEventLineupEntry)
              .filter((item) => toId(item.eventLineupId) === lineupId),
          );

          return { eventLineup, entries };
        } catch {
          return { eventLineup, entries: [] };
        }
      },
    });
  },
};
