/* eslint-disable @typescript-eslint/member-ordering */
import {
  DestroyRef,
  Injectable,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import type {
  PluginRequirementSummary,
  PluginRequirementsSnapshot,
  TojuPluginManifest
} from '../../../../shared-kernel';
import { RealtimeSessionFacade } from '../../../../core/realtime';
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
import { selectCurrentRoom, selectCurrentRoomId } from '../../../../store/rooms/rooms.selectors';
import { ServerDirectoryFacade, type ServerSourceSelector } from '../../../server-directory';
import { PluginRegistryService } from './plugin-registry.service';
import { PluginRequirementService } from './plugin-requirement.service';

const STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS = 'metoyou_optional_plugin_requirement_dismissals';

type RequirementDismissalState = Record<string, Record<string, number>>;

export type PluginRequirementComparisonStatus =
  | 'blockedByServer'
  | 'disabled'
  | 'enabled'
  | 'incompatible'
  | 'missing'
  | 'notRequired';

export interface PluginRequirementComparison {
  installed?: TojuPluginManifest;
  pluginId: string;
  requirement?: PluginRequirementSummary;
  status: PluginRequirementComparisonStatus;
}

@Injectable({ providedIn: 'root' })
export class PluginRequirementStateService {
  private readonly destroyRef = inject(DestroyRef);
  private readonly pluginRequirements = inject(PluginRequirementService);
  private readonly realtime = inject(RealtimeSessionFacade);
  private readonly registry = inject(PluginRegistryService);
  private readonly serverDirectory = inject(ServerDirectoryFacade);
  private readonly store = inject(Store);

  private readonly currentRoom = this.store.selectSignal(selectCurrentRoom);
  private readonly currentRoomId = this.store.selectSignal(selectCurrentRoomId);
  private readonly snapshotsSignal = signal<Record<string, PluginRequirementsSnapshot>>({});
  private readonly refreshErrorsSignal = signal<Record<string, string>>({});
  private readonly sessionDismissedOptionalSignal = signal<Record<string, string[]>>({});
  private readonly hiddenOptionalSignal = signal<RequirementDismissalState>(loadRequirementDismissals());

  readonly currentSnapshot = computed(() => {
    const roomId = this.currentRoomId();

    return roomId ? this.snapshotsSignal()[roomId] ?? null : null;
  });
  readonly refreshErrors = this.refreshErrorsSignal.asReadonly();
  readonly missingInstallableRequirements = computed(() => {
    const requirements: PluginRequirementSummary[] = [];

    for (const comparison of this.comparisons()) {
      if (this.isMissingInstallableRequirement(comparison) && comparison.requirement) {
        requirements.push(comparison.requirement);
      }
    }

    return requirements;
  });
  readonly missingRequiredRequirements = computed(() => this.missingInstallableRequirements()
    .filter((requirement) => requirement.status === 'required'));
  readonly visibleOptionalRequirements = computed(() => this.missingInstallableRequirements()
    .filter((requirement) => requirement.status === 'optional' || requirement.status === 'recommended')
    .filter((requirement) => !this.isOptionalRequirementDismissed(requirement)));
  readonly comparisons = computed<PluginRequirementComparison[]>(() => {
    const snapshot = this.currentSnapshot();
    const installedEntries = this.registry.entries();
    const installedById = new Map(installedEntries.map((entry) => [entry.manifest.id, entry]));
    const requirementIds = new Set(snapshot?.requirements.map((requirement) => requirement.pluginId) ?? []);
    const comparisons: PluginRequirementComparison[] = [];

    for (const requirement of snapshot?.requirements ?? []) {
      const entry = installedById.get(requirement.pluginId);

      comparisons.push({
        installed: entry?.manifest,
        pluginId: requirement.pluginId,
        requirement,
        status: this.resolveStatus(requirement, entry)
      });
    }

    for (const entry of installedEntries) {
      if (!requirementIds.has(entry.manifest.id)) {
        comparisons.push({
          installed: entry.manifest,
          pluginId: entry.manifest.id,
          status: entry.enabled ? 'enabled' : 'disabled'
        });
      }
    }

    return comparisons.sort((left, right) => left.pluginId.localeCompare(right.pluginId));
  });

  constructor() {
    this.realtime.onSignalingMessage
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((message) => {
        if ((message.type === 'plugin_requirements' || message.type === 'plugin_requirements_changed') && isSnapshotMessage(message)) {
          this.setSnapshot(message.serverId, message.snapshot);
        }
      });

    effect(() => {
      const roomId = this.currentRoomId();

      if (roomId) {
        void this.refreshCurrent();
      }
    });
  }

  async refreshCurrent(): Promise<void> {
    const roomId = this.currentRoomId();

    if (!roomId) {
      return;
    }

    try {
      const apiBaseUrl = this.serverDirectory.getApiBaseUrl(this.currentRoomSourceSelector());
      const snapshot = await new Promise<PluginRequirementsSnapshot>((resolve, reject) => {
        this.pluginRequirements.getSnapshot(apiBaseUrl, roomId).subscribe({
          error: reject,
          next: resolve
        });
      });

      this.setSnapshot(roomId, snapshot);
      this.refreshErrorsSignal.update((errors) => {
        const { [roomId]: _removed, ...next } = errors;

        return next;
      });
    } catch (error) {
      this.refreshErrorsSignal.update((errors) => ({
        ...errors,
        [roomId]: error instanceof Error ? error.message : 'Unable to refresh plugin requirements'
      }));
    }
  }

  comparisonFor(pluginId: string): PluginRequirementComparison | null {
    return this.comparisons().find((comparison) => comparison.pluginId === pluginId) ?? null;
  }

  dismissOptionalRequirement(requirement: PluginRequirementSummary, options: { persist?: boolean } = {}): void {
    const roomId = this.currentRoomId();

    if (!roomId) {
      return;
    }

    if (options.persist) {
      this.hiddenOptionalSignal.update((dismissals) => {
        const nextDismissals = {
          ...dismissals,
          [roomId]: {
            ...(dismissals[roomId] ?? {}),
            [requirement.pluginId]: requirement.updatedAt
          }
        };

        saveRequirementDismissals(nextDismissals);
        return nextDismissals;
      });

      return;
    }

    this.sessionDismissedOptionalSignal.update((dismissals) => ({
      ...dismissals,
      [roomId]: Array.from(new Set([...(dismissals[roomId] ?? []), requirement.pluginId]))
    }));
  }

  private setSnapshot(serverId: string, snapshot: PluginRequirementsSnapshot): void {
    this.snapshotsSignal.update((snapshots) => ({
      ...snapshots,
      [serverId]: snapshot
    }));
  }

  private currentRoomSourceSelector(): ServerSourceSelector | undefined {
    const room = this.currentRoom();

    if (!room?.sourceId && !room?.sourceUrl) {
      return undefined;
    }

    return {
      sourceId: room.sourceId,
      sourceUrl: room.sourceUrl
    };
  }

  private resolveStatus(
    requirement: PluginRequirementSummary,
    entry: { enabled: boolean; manifest: TojuPluginManifest } | undefined
  ): PluginRequirementComparisonStatus {
    if (requirement.status === 'blocked') {
      return 'blockedByServer';
    }

    if (requirement.status === 'incompatible') {
      return 'incompatible';
    }

    if (!entry) {
      return 'missing';
    }

    if (!entry.enabled) {
      return 'disabled';
    }

    if (requirement.versionRange && !isVersionCompatible(entry.manifest.version, requirement.versionRange)) {
      return 'incompatible';
    }

    return 'enabled';
  }

  private isMissingInstallableRequirement(comparison: PluginRequirementComparison): boolean {
    const requirement = comparison.requirement;

    return comparison.status === 'missing'
      && !!requirement
      && (requirement.status === 'required' || requirement.status === 'optional' || requirement.status === 'recommended')
      && (!!requirement.manifest || !!requirement.installUrl);
  }

  private isOptionalRequirementDismissed(requirement: PluginRequirementSummary): boolean {
    const roomId = this.currentRoomId();

    if (!roomId) {
      return true;
    }

    if ((this.sessionDismissedOptionalSignal()[roomId] ?? []).includes(requirement.pluginId)) {
      return true;
    }

    const hiddenAt = this.hiddenOptionalSignal()[roomId]?.[requirement.pluginId];

    return typeof hiddenAt === 'number' && hiddenAt >= requirement.updatedAt;
  }
}

function loadRequirementDismissals(): RequirementDismissalState {
  try {
    const rawValue = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS));

    if (!rawValue) {
      return {};
    }

    return normalizeRequirementDismissals(JSON.parse(rawValue) as unknown);
  } catch {
    return {};
  }
}

function saveRequirementDismissals(dismissals: RequirementDismissalState): void {
  try {
    localStorage.setItem(getUserScopedStorageKey(STORAGE_KEY_OPTIONAL_REQUIREMENT_DISMISSALS), JSON.stringify(dismissals));
  } catch {}
}

function normalizeRequirementDismissals(value: unknown): RequirementDismissalState {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return {};
  }

  return Object.fromEntries(Object.entries(value as Record<string, unknown>)
    .map(([serverId, serverValue]) => [serverId, normalizeServerDismissals(serverValue)])
    .filter(([, serverValue]) => Object.keys(serverValue).length > 0));
}

function normalizeServerDismissals(value: unknown): Record<string, number> {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return {};
  }

  return Object.fromEntries(Object.entries(value as Record<string, unknown>)
    .filter((entry): entry is [string, number] => typeof entry[1] === 'number'));
}

function isSnapshotMessage(message: unknown): message is { serverId: string; snapshot: PluginRequirementsSnapshot } {
  const record = message as Record<string, unknown>;

  return typeof record['serverId'] === 'string'
    && !!record['snapshot']
    && typeof record['snapshot'] === 'object';
}

function isVersionCompatible(version: string, versionRange: string): boolean {
  const normalizedRange = versionRange.trim();

  if (!normalizedRange || normalizedRange === '*') {
    return true;
  }

  if (normalizedRange.startsWith('^')) {
    return version.split('.')[0] === normalizedRange.slice(1).split('.')[0];
  }

  if (normalizedRange.startsWith('~')) {
    const [major, minor] = version.split('.');
    const [rangeMajor, rangeMinor] = normalizedRange.slice(1).split('.');

    return major === rangeMajor && minor === rangeMinor;
  }

  return version === normalizedRange;
}
