import { app, net } from 'electron';
import { autoUpdater } from 'electron-updater';
import { readDesktopSettings, type AutoUpdateMode } from '../desktop-settings';
import { getMainWindow } from '../window/create-window';
import {
  compareSemanticVersions,
  normalizeSemanticVersion,
  sortSemanticVersionsDescending
} from './version';

const DEFAULT_POLL_INTERVAL_MINUTES = 30;
const MINIMUM_POLL_INTERVAL_MINUTES = 5;

interface ReleaseManifestEntry {
  feedUrl: string;
  notes?: string;
  publishedAt?: string;
  version: string;
}

interface ServerHealthResponse {
  releaseManifestUrl?: string;
  serverVersion?: string;
}

interface UpdateVersionInfo {
  version: string;
}

interface ReleaseManifest {
  minimumServerVersion: string | null;
  pollIntervalMinutes: number;
  versions: ReleaseManifestEntry[];
}

export type DesktopUpdateStatus =
  | 'idle'
  | 'disabled'
  | 'checking'
  | 'downloading'
  | 'up-to-date'
  | 'restart-required'
  | 'unsupported'
  | 'no-manifest'
  | 'target-unavailable'
  | 'target-older-than-installed'
  | 'error';

export type DesktopUpdateServerVersionStatus =
  | 'unknown'
  | 'reported'
  | 'missing'
  | 'unavailable';

export interface DesktopUpdateServerContext {
  manifestUrls: string[];
  serverVersion: string | null;
  serverVersionStatus: DesktopUpdateServerVersionStatus;
}

export interface DesktopUpdateServerHealthSnapshot {
  manifestUrl: string | null;
  serverVersion: string | null;
  serverVersionStatus: DesktopUpdateServerVersionStatus;
}

export interface DesktopUpdateState {
  autoUpdateMode: AutoUpdateMode;
  availableVersions: string[];
  configuredManifestUrls: string[];
  currentVersion: string;
  defaultManifestUrls: string[];
  isSupported: boolean;
  lastCheckedAt: number | null;
  latestVersion: string | null;
  manifestUrl: string | null;
  manifestUrls: string[];
  minimumServerVersion: string | null;
  preferredVersion: string | null;
  restartRequired: boolean;
  serverBlocked: boolean;
  serverBlockMessage: string | null;
  serverVersion: string | null;
  serverVersionStatus: DesktopUpdateServerVersionStatus;
  status: DesktopUpdateStatus;
  statusMessage: string | null;
  targetVersion: string | null;
}

export const AUTO_UPDATE_STATE_CHANGED_CHANNEL = 'auto-update-state-changed';

const SERVER_HEALTH_TIMEOUT_MS = 5_000;

let currentCheckPromise: Promise<void> | null = null;
let currentContext: DesktopUpdateServerContext = {
  manifestUrls: [],
  serverVersion: null,
  serverVersionStatus: 'unknown'
};
let desktopUpdateState: DesktopUpdateState = createInitialState();
let periodicRefreshTimer: NodeJS.Timeout | null = null;
let refreshGeneration = 0;
let updaterInitialized = false;

function createInitialState(): DesktopUpdateState {
  const settings = readDesktopSettings();

  return {
    autoUpdateMode: settings.autoUpdateMode,
    availableVersions: [],
    configuredManifestUrls: settings.manifestUrls,
    currentVersion: app.getVersion(),
    defaultManifestUrls: [],
    isSupported: app.isPackaged,
    lastCheckedAt: null,
    latestVersion: null,
    manifestUrl: null,
    manifestUrls: settings.manifestUrls,
    minimumServerVersion: null,
    preferredVersion: settings.preferredVersion,
    restartRequired: false,
    serverBlocked: false,
    serverBlockMessage: null,
    serverVersion: null,
    serverVersionStatus: 'unknown',
    status: 'idle',
    statusMessage: null,
    targetVersion: null
  };
}

function clearPeriodicRefreshTimer(): void {
  if (!periodicRefreshTimer) {
    return;
  }

  clearInterval(periodicRefreshTimer);
  periodicRefreshTimer = null;
}

function emitState(): void {
  const mainWindow = getMainWindow();

  if (!mainWindow || mainWindow.isDestroyed()) {
    return;
  }

  mainWindow.webContents.send(AUTO_UPDATE_STATE_CHANGED_CHANNEL, desktopUpdateState);
}

function setDesktopUpdateState(patch: Partial<DesktopUpdateState>): void {
  desktopUpdateState = {
    ...desktopUpdateState,
    ...patch,
    currentVersion: app.getVersion()
  };

  emitState();
}

function sanitizeHttpUrl(rawValue: unknown): string | null {
  if (typeof rawValue !== 'string') {
    return null;
  }

  const trimmedValue = rawValue.trim();

  if (!trimmedValue) {
    return null;
  }

  try {
    const parsedUrl = new URL(trimmedValue);

    return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'
      ? parsedUrl.toString().replace(/\/$/, '')
      : null;
  } catch {
    return null;
  }
}

function sanitizeHttpUrls(rawValue: unknown): string[] {
  if (!Array.isArray(rawValue)) {
    return [];
  }

  const manifestUrls: string[] = [];

  for (const entry of rawValue) {
    const manifestUrl = sanitizeHttpUrl(entry);

    if (!manifestUrl || manifestUrls.includes(manifestUrl)) {
      continue;
    }

    manifestUrls.push(manifestUrl);
  }

  return manifestUrls;
}

function getEffectiveManifestUrls(configuredManifestUrls: string[]): string[] {
  return configuredManifestUrls.length > 0
    ? [...configuredManifestUrls]
    : [...currentContext.manifestUrls];
}

function coercePollIntervalMinutes(value: unknown): number {
  return typeof value === 'number' && Number.isFinite(value)
    ? Math.max(MINIMUM_POLL_INTERVAL_MINUTES, Math.round(value))
    : DEFAULT_POLL_INTERVAL_MINUTES;
}

function parseReleaseManifest(payload: unknown): ReleaseManifest {
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
    throw new Error('Release manifest must be a JSON object.');
  }

  const rawManifest = payload as Record<string, unknown>;
  const rawVersions = Array.isArray(rawManifest.versions) ? rawManifest.versions : [];
  const parsedVersions: ReleaseManifestEntry[] = [];

  for (const entry of rawVersions) {
    if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
      continue;
    }

    const rawEntry = entry as Record<string, unknown>;
    const version = normalizeSemanticVersion(rawEntry.version as string | undefined);
    const feedUrl = sanitizeHttpUrl(rawEntry.feedUrl);

    if (!version || !feedUrl) {
      continue;
    }

    const notes = typeof rawEntry.notes === 'string' && rawEntry.notes.trim().length > 0
      ? rawEntry.notes.trim()
      : undefined;
    const publishedAt = typeof rawEntry.publishedAt === 'string'
      && rawEntry.publishedAt.trim().length > 0
      ? rawEntry.publishedAt.trim()
      : undefined;

    parsedVersions.push({
      feedUrl,
      ...(notes ? { notes } : {}),
      ...(publishedAt ? { publishedAt } : {}),
      version
    });
  }

  parsedVersions.sort((left, right) => compareSemanticVersions(right.version, left.version));

  const deduplicatedVersions: ReleaseManifestEntry[] = [];
  const seenVersions = new Set<string>();

  for (const entry of parsedVersions) {
    if (seenVersions.has(entry.version)) {
      continue;
    }

    seenVersions.add(entry.version);
    deduplicatedVersions.push(entry);
  }

  if (deduplicatedVersions.length === 0) {
    throw new Error('Release manifest does not contain any valid versions.');
  }

  return {
    minimumServerVersion: normalizeSemanticVersion(
      rawManifest.minimumServerVersion as string | undefined
    ),
    pollIntervalMinutes: coercePollIntervalMinutes(rawManifest.pollIntervalMinutes),
    versions: deduplicatedVersions
  };
}

function resolveServerCompatibility(minimumServerVersion: string | null): {
  blocked: boolean;
  message: string | null;
} {
  if (currentContext.serverVersionStatus === 'missing') {
    return {
      blocked: true,
      message: 'The connected server is too old. Update the server project to a version that reports its release metadata.'
    };
  }

  if (
    currentContext.serverVersionStatus !== 'reported'
    || !minimumServerVersion
    || !currentContext.serverVersion
  ) {
    return { blocked: false,
      message: null };
  }

  if (compareSemanticVersions(currentContext.serverVersion, minimumServerVersion) >= 0) {
    return { blocked: false,
      message: null };
  }

  return {
    blocked: true,
    message: `This desktop app requires server version ${minimumServerVersion} or newer. The connected server is ${currentContext.serverVersion}.`
  };
}

function getManifestMissingMessage(autoUpdateMode: AutoUpdateMode): string {
  return autoUpdateMode === 'off'
    ? 'Automatic updates are turned off.'
    : 'No release manifest URLs are configured.';
}

function getTargetUnavailableMessage(
  autoUpdateMode: AutoUpdateMode,
  preferredVersion: string | null
): string {
  if (autoUpdateMode === 'version') {
    return preferredVersion
      ? `The selected version ${preferredVersion} is not present in the release manifest.`
      : 'Select a version before enabling pinned updates.';
  }

  return 'No compatible release was found in the release manifest.';
}

function getFriendlyErrorMessage(error: unknown): string {
  const rawMessage = error instanceof Error
    ? error.message
    : typeof error === 'string'
      ? error
      : 'Unknown error';

  if (rawMessage.includes('APPIMAGE')) {
    return 'Automatic updates on Linux require the packaged AppImage or DEB build.';
  }

  return rawMessage;
}

function getSelectedRelease(manifest: ReleaseManifest): ReleaseManifestEntry | null {
  const settings = readDesktopSettings();

  if (settings.autoUpdateMode === 'version') {
    const preferredVersion = normalizeSemanticVersion(settings.preferredVersion);

    if (!preferredVersion) {
      return null;
    }

    return manifest.versions.find((entry) => entry.version === preferredVersion) ?? null;
  }

  return manifest.versions[0] ?? null;
}

function refreshSettingsSnapshot(): void {
  const settings = readDesktopSettings();
  const defaultManifestUrls = [...currentContext.manifestUrls];
  const manifestUrls = getEffectiveManifestUrls(settings.manifestUrls);

  setDesktopUpdateState({
    autoUpdateMode: settings.autoUpdateMode,
    configuredManifestUrls: settings.manifestUrls,
    currentVersion: app.getVersion(),
    defaultManifestUrls,
    isSupported: app.isPackaged,
    manifestUrls,
    preferredVersion: settings.preferredVersion
  });
}

function schedulePeriodicRefresh(pollIntervalMinutes: number): void {
  clearPeriodicRefreshTimer();

  if (!app.isPackaged || desktopUpdateState.autoUpdateMode === 'off' || desktopUpdateState.restartRequired) {
    return;
  }

  periodicRefreshTimer = setInterval(() => {
    void refreshDesktopUpdater('scheduled');
  }, pollIntervalMinutes * 60_000);
}

async function loadReleaseManifest(manifestUrl: string): Promise<ReleaseManifest> {
  const response = await net.fetch(manifestUrl, {
    headers: {
      accept: 'application/json'
    }
  });

  if (!response.ok) {
    throw new Error(`Release manifest request failed with status ${response.status}.`);
  }

  const payload = await response.json();

  return parseReleaseManifest(payload);
}

function createUnavailableServerHealthSnapshot(): DesktopUpdateServerHealthSnapshot {
  return {
    manifestUrl: null,
    serverVersion: null,
    serverVersionStatus: 'unavailable'
  };
}

async function loadServerHealth(serverUrl: string): Promise<DesktopUpdateServerHealthSnapshot> {
  const sanitizedServerUrl = sanitizeHttpUrl(serverUrl);

  if (!sanitizedServerUrl) {
    return createUnavailableServerHealthSnapshot();
  }

  try {
    const response = await net.fetch(`${sanitizedServerUrl}/api/health`, {
      method: 'GET',
      headers: {
        accept: 'application/json'
      },
      signal: AbortSignal.timeout(SERVER_HEALTH_TIMEOUT_MS)
    });

    if (!response.ok) {
      return createUnavailableServerHealthSnapshot();
    }

    const payload = await response.json() as ServerHealthResponse;
    const serverVersion = normalizeSemanticVersion(payload.serverVersion);

    return {
      manifestUrl: sanitizeHttpUrl(payload.releaseManifestUrl),
      serverVersion,
      serverVersionStatus: serverVersion ? 'reported' : 'missing'
    };
  } catch {
    return createUnavailableServerHealthSnapshot();
  }
}

function formatManifestLoadErrors(errors: string[]): string {
  if (errors.length === 0) {
    return 'No valid release manifest could be loaded.';
  }

  if (errors.length === 1) {
    return errors[0];
  }

  const remainingCount = errors.length - 1;

  return `${errors[0]} (${remainingCount} more manifest URL${remainingCount === 1 ? '' : 's'} failed.)`;
}

async function loadReleaseManifestFromCandidates(manifestUrls: string[]): Promise<{
  manifest: ReleaseManifest;
  manifestUrl: string;
}> {
  const errors: string[] = [];

  for (const manifestUrl of manifestUrls) {
    try {
      const manifest = await loadReleaseManifest(manifestUrl);

      return {
        manifest,
        manifestUrl
      };
    } catch (error) {
      errors.push(`${manifestUrl}: ${getFriendlyErrorMessage(error)}`);
    }
  }

  throw new Error(formatManifestLoadErrors(errors));
}

async function performUpdateCheck(
  targetRelease: ReleaseManifestEntry,
  generation: number
): Promise<void> {
  if (generation !== refreshGeneration || desktopUpdateState.restartRequired) {
    return;
  }

  if (currentCheckPromise) {
    await currentCheckPromise;
    return;
  }

  currentCheckPromise = (async () => {
    autoUpdater.autoDownload = true;
    autoUpdater.autoInstallOnAppQuit = true;
    autoUpdater.allowDowngrade = false;
    autoUpdater.setFeedURL({ provider: 'generic', url: targetRelease.feedUrl });

    setDesktopUpdateState({
      lastCheckedAt: Date.now(),
      status: 'checking',
      statusMessage: `Checking for MetoYou ${targetRelease.version}...`,
      targetVersion: targetRelease.version
    });

    await autoUpdater.checkForUpdates();
  })();

  try {
    await currentCheckPromise;
  } finally {
    currentCheckPromise = null;
  }
}

async function refreshDesktopUpdater(
  _reason: 'context-changed' | 'manual' | 'scheduled' | 'settings-changed'
): Promise<void> {
  const generation = ++refreshGeneration;

  refreshSettingsSnapshot();

  const manifestUrls = [...desktopUpdateState.manifestUrls];
  const baseServerCompatibility = resolveServerCompatibility(null);

  setDesktopUpdateState({
    manifestUrl: null,
    serverBlocked: baseServerCompatibility.blocked,
    serverBlockMessage: baseServerCompatibility.message,
    serverVersion: currentContext.serverVersion,
    serverVersionStatus: currentContext.serverVersionStatus
  });

  if (manifestUrls.length === 0) {
    clearPeriodicRefreshTimer();

    if (desktopUpdateState.restartRequired) {
      setDesktopUpdateState({
        availableVersions: [],
        latestVersion: null,
        manifestUrl: null,
        minimumServerVersion: null
      });

      return;
    }

    setDesktopUpdateState({
      availableVersions: [],
      latestVersion: null,
      manifestUrl: null,
      minimumServerVersion: null,
      status: app.isPackaged && desktopUpdateState.autoUpdateMode !== 'off'
        ? 'no-manifest'
        : 'disabled',
      statusMessage: getManifestMissingMessage(desktopUpdateState.autoUpdateMode),
      targetVersion: null
    });

    if (!app.isPackaged && desktopUpdateState.autoUpdateMode !== 'off') {
      setDesktopUpdateState({
        status: 'unsupported',
        statusMessage: 'Automatic updates only work in packaged desktop builds.'
      });
    }

    return;
  }

  try {
    const manifestResult = await loadReleaseManifestFromCandidates(manifestUrls);

    if (generation !== refreshGeneration) {
      return;
    }

    const { manifest, manifestUrl } = manifestResult;
    const availableVersions = sortSemanticVersionsDescending(
      manifest.versions.map((entry) => entry.version)
    );
    const latestVersion = availableVersions[0] ?? null;
    const selectedRelease = getSelectedRelease(manifest);
    const serverCompatibility = resolveServerCompatibility(
      manifest.minimumServerVersion
    );

    setDesktopUpdateState({
      availableVersions,
      latestVersion,
      manifestUrl,
      minimumServerVersion: manifest.minimumServerVersion,
      serverBlocked: serverCompatibility.blocked,
      serverBlockMessage: serverCompatibility.message,
      targetVersion: selectedRelease?.version ?? null
    });

    if (desktopUpdateState.restartRequired) {
      clearPeriodicRefreshTimer();
      return;
    }

    if (!app.isPackaged) {
      clearPeriodicRefreshTimer();

      setDesktopUpdateState({
        status: 'unsupported',
        statusMessage: 'Automatic updates only work in packaged desktop builds.'
      });

      return;
    }

    if (desktopUpdateState.autoUpdateMode === 'off') {
      clearPeriodicRefreshTimer();

      setDesktopUpdateState({
        status: 'disabled',
        statusMessage: 'Automatic updates are turned off.'
      });

      return;
    }

    if (!selectedRelease) {
      clearPeriodicRefreshTimer();

      setDesktopUpdateState({
        status: 'target-unavailable',
        statusMessage: getTargetUnavailableMessage(
          desktopUpdateState.autoUpdateMode,
          desktopUpdateState.preferredVersion
        )
      });

      return;
    }

    if (compareSemanticVersions(selectedRelease.version, app.getVersion()) < 0) {
      clearPeriodicRefreshTimer();

      setDesktopUpdateState({
        status: 'target-older-than-installed',
        statusMessage: `MetoYou ${app.getVersion()} is newer than ${selectedRelease.version}. Downgrades are not applied automatically.`,
        targetVersion: selectedRelease.version
      });

      return;
    }

    schedulePeriodicRefresh(manifest.pollIntervalMinutes);
    await performUpdateCheck(selectedRelease, generation);
  } catch (error) {
    if (generation !== refreshGeneration) {
      return;
    }

    clearPeriodicRefreshTimer();

    setDesktopUpdateState({
      availableVersions: [],
      latestVersion: null,
      manifestUrl: null,
      minimumServerVersion: null,
      status: 'error',
      statusMessage: getFriendlyErrorMessage(error),
      targetVersion: null
    });
  }
}

export function getDesktopUpdateState(): DesktopUpdateState {
  return desktopUpdateState;
}

export function initializeDesktopUpdater(): void {
  if (updaterInitialized) {
    return;
  }

  updaterInitialized = true;

  autoUpdater.on('checking-for-update', () => {
    if (desktopUpdateState.restartRequired) {
      return;
    }

    setDesktopUpdateState({
      status: 'checking',
      statusMessage: 'Checking for desktop updates...'
    });
  });

  autoUpdater.on('update-available', (updateInfo: UpdateVersionInfo) => {
    const nextVersion = normalizeSemanticVersion(updateInfo.version)
      ?? desktopUpdateState.targetVersion;

    setDesktopUpdateState({
      lastCheckedAt: Date.now(),
      status: 'downloading',
      statusMessage: `Downloading MetoYou ${nextVersion ?? 'update'}...`,
      targetVersion: nextVersion
    });
  });

  autoUpdater.on('update-not-available', () => {
    if (desktopUpdateState.restartRequired) {
      return;
    }

    const isPinnedVersion = desktopUpdateState.autoUpdateMode === 'version'
      && !!desktopUpdateState.targetVersion;

    setDesktopUpdateState({
      lastCheckedAt: Date.now(),
      status: 'up-to-date',
      statusMessage: isPinnedVersion
        ? `MetoYou ${desktopUpdateState.targetVersion} is already installed.`
        : 'MetoYou is up to date.'
    });
  });

  autoUpdater.on('update-downloaded', (updateInfo: UpdateVersionInfo) => {
    clearPeriodicRefreshTimer();

    const nextVersion = normalizeSemanticVersion(updateInfo.version)
      ?? desktopUpdateState.targetVersion;

    setDesktopUpdateState({
      lastCheckedAt: Date.now(),
      restartRequired: true,
      status: 'restart-required',
      statusMessage: `MetoYou ${nextVersion ?? 'update'} is ready. Restart the app to finish installing it.`,
      targetVersion: nextVersion
    });
  });

  autoUpdater.on('error', (error: unknown) => {
    if (desktopUpdateState.restartRequired) {
      return;
    }

    setDesktopUpdateState({
      lastCheckedAt: Date.now(),
      status: 'error',
      statusMessage: getFriendlyErrorMessage(error)
    });
  });

  refreshSettingsSnapshot();
}

export async function configureDesktopUpdaterContext(
  context: Partial<DesktopUpdateServerContext>
): Promise<DesktopUpdateState> {
  initializeDesktopUpdater();

  const manifestUrls = sanitizeHttpUrls(context.manifestUrls);

  currentContext = {
    manifestUrls,
    serverVersion: normalizeSemanticVersion(context.serverVersion),
    serverVersionStatus: context.serverVersionStatus ?? 'unknown'
  };

  await refreshDesktopUpdater('context-changed');
  return desktopUpdateState;
}

export async function handleDesktopSettingsChanged(): Promise<void> {
  initializeDesktopUpdater();
  await refreshDesktopUpdater('settings-changed');
}

export async function checkForDesktopUpdates(): Promise<DesktopUpdateState> {
  initializeDesktopUpdater();
  await refreshDesktopUpdater('manual');
  return desktopUpdateState;
}

export async function readDesktopUpdateServerHealth(
  serverUrl: string
): Promise<DesktopUpdateServerHealthSnapshot> {
  return await loadServerHealth(serverUrl);
}

export function restartToApplyUpdate(): boolean {
  if (!desktopUpdateState.restartRequired) {
    return false;
  }

  autoUpdater.quitAndInstall(true, true);
  return true;
}

export function shutdownDesktopUpdater(): void {
  clearPeriodicRefreshTimer();
}
