import {
  Injectable,
  computed,
  inject,
  signal
} from '@angular/core';
import { getUserScopedStorageKey } from '../../../../core/storage/current-user-storage';
import type { PluginCapabilityId, TojuPluginManifest } from '../../../../shared-kernel';
import { PluginDesktopStateService } from './plugin-desktop-state.service';

const STORAGE_KEY_PLUGIN_CAPABILITIES = 'metoyou_plugin_capability_grants';

export class PluginCapabilityError extends Error {
  constructor(pluginId: string, capability: PluginCapabilityId) {
    super(`Plugin ${pluginId} needs capability ${capability}`);
    this.name = 'PluginCapabilityError';
  }
}

@Injectable({ providedIn: 'root' })
export class PluginCapabilityService {
  readonly grants = computed(() => this.grantsSignal());

  private readonly desktopState = inject(PluginDesktopStateService);
  private readonly grantsSignal = signal<Record<string, PluginCapabilityId[]>>(this.loadGrants());

  constructor() {
    void this.loadDesktopGrants();
  }

  grant(pluginId: string, capability: PluginCapabilityId): void {
    this.grantsSignal.update((grants) => ({
      ...grants,
      [pluginId]: Array.from(new Set([...(grants[pluginId] ?? []), capability])).sort()
    }));

    void this.saveGrants();
  }

  grantAll(manifest: TojuPluginManifest): void {
    this.grantsSignal.update((grants) => ({
      ...grants,
      [manifest.id]: [...(manifest.capabilities ?? [])].sort()
    }));

    void this.saveGrants();
  }

  revoke(pluginId: string, capability: PluginCapabilityId): void {
    this.grantsSignal.update((grants) => ({
      ...grants,
      [pluginId]: (grants[pluginId] ?? []).filter((entry) => entry !== capability)
    }));

    void this.saveGrants();
  }

  revokeAll(pluginId: string): void {
    this.grantsSignal.update((grants) => {
      const { [pluginId]: _removed, ...next } = grants;

      return next;
    });

    void this.saveGrants();
  }

  has(pluginId: string, capability: PluginCapabilityId): boolean {
    return this.grants()[pluginId]?.includes(capability) ?? false;
  }

  assert(pluginId: string, capability: PluginCapabilityId): void {
    if (!this.has(pluginId, capability)) {
      throw new PluginCapabilityError(pluginId, capability);
    }
  }

  missing(manifest: TojuPluginManifest): PluginCapabilityId[] {
    return (manifest.capabilities ?? []).filter((capability) => !this.has(manifest.id, capability));
  }

  private loadGrants(): Record<string, PluginCapabilityId[]> {
    try {
      const raw = localStorage.getItem(getUserScopedStorageKey(STORAGE_KEY_PLUGIN_CAPABILITIES));

      if (!raw) {
        return {};
      }

      const parsed = JSON.parse(raw) as unknown;

      return isGrantRecord(parsed) ? parsed : {};
    } catch {
      return {};
    }
  }

  private async loadDesktopGrants(): Promise<void> {
    const grants = await this.desktopState.readJson<Record<string, PluginCapabilityId[]>>(STORAGE_KEY_PLUGIN_CAPABILITIES, this.grantsSignal());

    if (isGrantRecord(grants)) {
      this.grantsSignal.set(grants);
    }
  }

  private async saveGrants(): Promise<void> {
    try {
      localStorage.setItem(
        getUserScopedStorageKey(STORAGE_KEY_PLUGIN_CAPABILITIES),
        JSON.stringify(this.grantsSignal())
      );
    } catch {}

    await this.desktopState.writeJson(STORAGE_KEY_PLUGIN_CAPABILITIES, this.grantsSignal());
  }
}

function isGrantRecord(value: unknown): value is Record<string, PluginCapabilityId[]> {
  return !!value
    && typeof value === 'object'
    && !Array.isArray(value)
    && Object.values(value).every((entry) => Array.isArray(entry) && entry.every((item) => typeof item === 'string'));
}
