import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListChecks, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Vote, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
import {
  hasUiCapability as hasUiCapabilityForModules,
  moduleInstalled as moduleInstalledForModules,
  moduleIntegrationEnabled as moduleIntegrationEnabledForModules,
  publicRouteContributionsForModules as publicRouteContributionsForModuleList,
  routeContributionsForModules as routeContributionsForModuleList,
  uiCapabilities as uiCapabilitiesForModules,
  uiCapability as uiCapabilityForModules } from
"./moduleLogic";
import { hasAnyScope, hasScope } from "../utils/permissions";
import {
  isViewSurfaceVisible,
  navigationViewSurfaceId,
  routeViewSurfaceId,
  viewSurfaceCatalogueForModules
} from "./views";

export const fallbackDashboardNavItem: PlatformNavItem =
{ to: "/dashboard", label: "i18n:govoplan-core.dashboard.d87f47b4", iconName: "dashboard", order: 10 };

type InstalledWebModuleLoader = {
  packageName: string;
  load: () => Promise<{ default: PlatformWebModule }>;
};

const installedWebModuleLoaders =
  installedWebModuleLoaderSource as unknown as InstalledWebModuleLoader[];

export function shellNavItemsForModules(modules: PlatformWebModule[]): PlatformNavItem[] {
  return moduleInstalledForModules("dashboard", modules) ? [] : [fallbackDashboardNavItem];
}

const localModuleCache = new Map<string, Promise<PlatformWebModule | null>>();
const loadedLocalModules = new Map<string, PlatformWebModule>();
const remoteModuleCache = new Map<string, Promise<PlatformWebModule | null>>();

declare global {
  interface Window {
    __GOVOPLAN_REMOTE_MODULE_KEYS__?: Record<string, JsonWebKey>;
  }
}

type RemoteAssetManifest = {
  contractVersion?: string;
  moduleId?: string;
  entry?: string;
  entryIntegrity?: string;
  moduleExport?: string;
};

const iconByName: Record<string, LucideIcon> = {
  activity: Activity,
  admin: Shield,
  bell: Bell,
  "book-user": BookUser,
  "briefcase-business": BriefcaseBusiness,
  building: Building2,
  "building-2": Building2,
  calendar: CalendarDays,
  "calendar-clock": CalendarClock,
  campaign: Mails,
  "clipboard-pen-line": ClipboardPenLine,
  dashboard: LayoutDashboard,
  "database-zap": DatabaseZap,
  file: Folder,
  files: Folder,
  folder: Folder,
  "folder-kanban": FolderKanban,
  form: Form,
  gavel: Gavel,
  inbox: Inbox,
  landmark: Landmark,
  "layout-template": LayoutTemplate,
  "list-tree": ListTree,
  "list-checks": ListChecks,
  mail: Mail,
  notifications: Bell,
  organizations: Building2,
  operator: RadioTower,
  "radio-tower": RadioTower,
  reports: ClipboardPenLine,
  "shield-check": ShieldCheck,
  templates: LayoutTemplate,
  users: Users,
  vote: Vote,
  waypoints: Waypoints,
  workflow: WorkflowIcon
};

export function iconComponentForName(iconName: string | null | undefined): LucideIcon | undefined {
  if (!iconName) return undefined;
  return iconByName[iconName];
}

function resolveNavItemIcon(item: PlatformNavItem): PlatformNavItem {
  return {
    ...item,
    icon: item.icon ?? iconComponentForName(item.iconName)
  };
}

function navFromMetadata(item: PlatformModuleInfo["nav"][number]): PlatformNavItem {
  return {
    to: item.path,
    label: item.label,
    iconName: item.icon ?? null,
    allOf: item.required_all,
    anyOf: item.required_any,
    order: item.order,
    surfaceId: item.surface_id ?? undefined
  };
}

function viewSurfaceFromMetadata(
  item: NonNullable<NonNullable<PlatformModuleInfo["frontend"]>["view_surfaces"]>[number]
): PlatformViewSurface {
  return {
    id: item.id,
    moduleId: item.module_id,
    kind: item.kind,
    label: item.label,
    parentId: item.parent_id,
    description: item.description,
    order: item.order,
    defaultVisible: item.default_visible,
    required: item.required
  };
}

function mergeViewSurfaces(
  module: PlatformWebModule,
  info: PlatformModuleInfo
): PlatformViewSurface[] | undefined {
  const surfaces = new Map<string, PlatformViewSurface>();
  for (const surface of info.frontend?.view_surfaces ?? []) {
    const normalized = viewSurfaceFromMetadata(surface);
    surfaces.set(normalized.id, normalized);
  }
  for (const surface of module.viewSurfaces ?? []) {
    surfaces.set(surface.id, {
      ...surfaces.get(surface.id),
      ...surface,
      required: Boolean(surfaces.get(surface.id)?.required || surface.required)
    });
  }
  return surfaces.size ? [...surfaces.values()] : undefined;
}

function routesWithServerMetadata(
  module: PlatformWebModule,
  info: PlatformModuleInfo
) {
  const routesByPath = new Map(
    (info.frontend?.routes ?? []).map((route) => [route.path, route])
  );
  return (module.routes ?? []).map((route) => {
    const metadata = routesByPath.get(route.path);
    return {
      ...route,
      surfaceId:
        metadata?.surface_id ??
        route.surfaceId ??
        routeViewSurfaceId(module.id, route.path)
    };
  });
}


function runtimeUiCapabilitiesForModule(module: PlatformWebModule, info: PlatformModuleInfo) {
  const enabledNames = new Set(info.runtime_ui_capabilities ?? []);
  if (enabledNames.size === 0 || !module.runtimeUiCapabilities) return {};
  return Object.fromEntries(
    Object.entries(module.runtimeUiCapabilities).filter(([name]) => enabledNames.has(name))
  );
}

function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo): PlatformWebModule {
  const backendNav = info.frontend?.nav.length ? info.frontend.nav : info.nav;
  return {
    ...module,
    label: info.name,
    version: info.version,
    dependencies: info.dependencies,
    optionalDependencies: info.optional_dependencies,
    remoteAssetManifest: info.frontend?.asset_manifest ?? module.remoteAssetManifest,
    remoteAssetSignature: info.frontend?.asset_manifest_signature ?? module.remoteAssetSignature,
    remoteAssetPublicKeyId: info.frontend?.asset_manifest_public_key_id ?? module.remoteAssetPublicKeyId,
    remoteAssetIntegrity: info.frontend?.asset_manifest_integrity ?? module.remoteAssetIntegrity,
    remoteAssetContractVersion: info.frontend?.asset_manifest_contract_version ?? module.remoteAssetContractVersion,
    navItems: backendNav.length ? backendNav.map(navFromMetadata) : module.navItems,
    routes: routesWithServerMetadata(module, info),
    publicRoutes: filterPublicRoutes(module, info.frontend?.public_routes),
    viewSurfaces: mergeViewSurfaces(module, info),
    uiCapabilities: {
      ...(module.uiCapabilities ?? {}),
      ...runtimeUiCapabilitiesForModule(module, info)
    }
  };
}

function filterPublicRoutes(
  module: PlatformWebModule,
  routes: NonNullable<PlatformModuleInfo["frontend"]>["public_routes"] | undefined
) {
  if (!routes?.length) return [];
  const allowedPaths = new Set(routes.map((route) => route.path));
  return (module.publicRoutes ?? []).filter((route) => allowedPaths.has(route.path));
}

function publicModuleInfo(info: PlatformPublicModuleInfo): PlatformModuleInfo {
  return {
    id: info.id,
    name: info.name,
    version: info.version,
    dependencies: [],
    optional_dependencies: [],
    enabled: true,
    runtime_ui_capabilities: [],
    nav: [],
    frontend: {
      ...info.frontend,
      routes: [],
      nav: [],
      settings_routes: []
    }
  };
}

export async function loadInstalledPublicWebModules(
  platformModules: PlatformPublicModuleInfo[] | null | undefined
): Promise<PlatformWebModule[]> {
  if (!platformModules?.length) return [];
  const loaded = await loadInstalledWebModules(platformModules.map(publicModuleInfo));
  return loaded.filter((module) => module.publicRoutes?.length);
}

export async function loadRemotePublicWebModules(
  platformModules: PlatformPublicModuleInfo[] | null | undefined,
  alreadyLoaded: PlatformWebModule[] = []
): Promise<PlatformWebModule[]> {
  if (!platformModules?.length) return [];
  const loaded = await loadRemoteWebModules(platformModules.map(publicModuleInfo), alreadyLoaded);
  return loaded.filter((module) => module.publicRoutes?.length);
}

export async function loadInstalledWebModules(
  platformModules: PlatformModuleInfo[] | null | undefined
): Promise<PlatformWebModule[]> {
  if (!platformModules?.length) return [];

  const enabledModules = platformModules.filter((module) => module.enabled);
  const resolved = await Promise.all(enabledModules.map(async (info) => {
    const local = await loadInstalledWebModule(info);
    return local ? applyServerMetadata(local, info) : null;
  }));
  return resolved.filter((module): module is PlatformWebModule => module !== null);
}

export async function loadRemoteWebModules(
platformModules: PlatformModuleInfo[] | null | undefined,
alreadyLoaded: PlatformWebModule[] = [])
: Promise<PlatformWebModule[]> {
  if (!platformModules?.length) return [];
  const loadedIds = new Set(alreadyLoaded.map((module) => module.id));
  const candidates = platformModules.filter((module) => {
    return module.enabled && !loadedIds.has(module.id) && Boolean(module.frontend?.asset_manifest);
  });
  if (candidates.length === 0) return [];
  const loaded = await Promise.all(candidates.map((module) => {
    const cacheKey = `${module.id}:${module.frontend?.asset_manifest ?? ""}:${module.version}`;
    let promise = remoteModuleCache.get(cacheKey);
    if (!promise) {
      promise = loadRemoteWebModule(module).catch((error) => {
        console.warn("GovOPlaN remote module was not loaded:", module.id, error);
        return null;
      });
      remoteModuleCache.set(cacheKey, promise);
    }
    return promise;
  }));
  return loaded.filter((module): module is PlatformWebModule => module !== null);
}

async function loadInstalledWebModule(info: PlatformModuleInfo): Promise<PlatformWebModule | null> {
  if (!info.frontend) return null;
  const packageName = info.frontend?.package_name;
  if (packageName) {
    const loader = installedWebModuleLoaders.find((candidate) => candidate.packageName === packageName);
    return loader ? loadInstalledWebModulePackage(loader, info.id) : null;
  }

  // Legacy manifests may omit package_name. Keep compatibility without making
  // every modern module eager: only this fallback imports all installed descriptors.
  const candidates = await Promise.all(
    installedWebModuleLoaders.map((loader) => loadInstalledWebModulePackage(loader))
  );
  return candidates.find((candidate) => candidate?.id === info.id) ?? null;
}

async function loadInstalledWebModulePackage(
  loader: InstalledWebModuleLoader,
  expectedModuleId?: string
): Promise<PlatformWebModule | null> {
  let promise = localModuleCache.get(loader.packageName);
  if (!promise) {
    promise = loader.load().
    then((imported) => {
      const module = imported.default;
      if (!isPlatformWebModule(module)) {
        throw new Error(`${loader.packageName} does not export a PlatformWebModule`);
      }
      loadedLocalModules.set(loader.packageName, module);
      return module;
    }).
    catch((error) => {
      localModuleCache.delete(loader.packageName);
      console.warn("GovOPlaN installed WebUI module was not loaded:", loader.packageName, error);
      return null;
    });
    localModuleCache.set(loader.packageName, promise);
  }
  const module = await promise;
  if (module && expectedModuleId && module.id !== expectedModuleId) {
    console.warn(
      "GovOPlaN installed WebUI package id mismatch:",
      loader.packageName,
      `expected ${expectedModuleId}, received ${module.id}`
    );
    return null;
  }
  return module;
}

async function loadRemoteWebModule(info: PlatformModuleInfo): Promise<PlatformWebModule | null> {
  const frontend = info.frontend;
  if (!frontend?.asset_manifest) return null;
  const manifestUrl = absoluteModuleAssetUrl(frontend.asset_manifest);
  const manifestResponse = await fetch(manifestUrl, { credentials: "same-origin" });
  if (!manifestResponse.ok) throw new Error(`manifest fetch failed with HTTP ${manifestResponse.status}`);
  const manifestBuffer = await manifestResponse.arrayBuffer();
  if (frontend.asset_manifest_integrity) {
    const ok = await verifyIntegrity(manifestBuffer, frontend.asset_manifest_integrity);
    if (!ok) throw new Error("i18n:govoplan-core.manifest_integrity_check_failed.6e3913bc");
  }
  const manifestText = new TextDecoder().decode(manifestBuffer);
  if (frontend.asset_manifest_signature && frontend.asset_manifest_public_key_id) {
    const signatureOk = await verifyManifestSignature(
      manifestBuffer,
      frontend.asset_manifest_signature,
      frontend.asset_manifest_public_key_id
    );
    if (signatureOk === false) throw new Error("i18n:govoplan-core.manifest_signature_check_failed.a9860176");
    if (signatureOk === null && !frontend.asset_manifest_integrity) throw new Error("i18n:govoplan-core.manifest_signature_key_is_not_trusted_by_this_sh.3bb2f629");
  } else if (!frontend.asset_manifest_integrity) {
    throw new Error("i18n:govoplan-core.remote_manifests_need_an_integrity_hash_or_a_ver.bfbb7daa");
  }
  const manifest = JSON.parse(manifestText) as RemoteAssetManifest;
  const contractVersion = manifest.contractVersion ?? frontend.asset_manifest_contract_version ?? "1";
  if (contractVersion !== "1") throw new Error(`unsupported remote asset contract ${contractVersion}`);
  if (manifest.moduleId && manifest.moduleId !== info.id) throw new Error(`manifest module id ${manifest.moduleId} does not match ${info.id}`);
  if (!manifest.entry) throw new Error("i18n:govoplan-core.remote_manifest_has_no_entry.8220dbc9");
  if (!manifest.entryIntegrity) throw new Error("i18n:govoplan-core.remote_entry_needs_an_integrity_hash.3b69ae0a");

  const entryUrl = absoluteModuleAssetUrl(manifest.entry, manifestUrl);
  const entryResponse = await fetch(entryUrl, { credentials: "same-origin" });
  if (!entryResponse.ok) throw new Error(`entry fetch failed with HTTP ${entryResponse.status}`);
  const entryBuffer = await entryResponse.arrayBuffer();
  if (!(await verifyIntegrity(entryBuffer, manifest.entryIntegrity))) throw new Error("i18n:govoplan-core.entry_integrity_check_failed.1d8f6b89");
  const blobUrl = URL.createObjectURL(new Blob([entryBuffer], { type: "text/javascript" }));
  try {
    const imported = (await import(/* @vite-ignore */blobUrl)) as Record<string, unknown>;
    const exported = manifest.moduleExport ? imported[manifest.moduleExport] : imported.default ?? imported.module;
    if (!isPlatformWebModule(exported)) throw new Error("i18n:govoplan-core.entry_did_not_export_a_platformwebmodule.9a157741");
    if (exported.id !== info.id) throw new Error(`entry module id ${exported.id} does not match ${info.id}`);
    return applyServerMetadata(exported, info);
  } finally {
    URL.revokeObjectURL(blobUrl);
  }
}

function absoluteModuleAssetUrl(value: string, base = window.location.origin): string {
  return new URL(value, base).toString();
}

async function verifyIntegrity(buffer: ArrayBuffer, integrity: string): Promise<boolean> {
  const separator = integrity.indexOf("-");
  const algorithm = separator > 0 ? integrity.slice(0, separator) : "";
  const expected = separator > 0 ? integrity.slice(separator + 1) : "";
  if (!algorithm || !expected) return false;
  const digestName = {
    "SHA-256": "SHA-256",
    "SHA-384": "SHA-384",
    "SHA-512": "SHA-512",
    SHA256: "SHA-256",
    SHA384: "SHA-384",
    SHA512: "SHA-512"
  }[algorithm.toUpperCase()];
  if (!digestName) return false;
  const digest = await crypto.subtle.digest(digestName, buffer);
  return constantTimeEqual(base64FromBuffer(digest), expected) || constantTimeEqual(hexFromBuffer(digest), expected.toLowerCase());
}

async function verifyManifestSignature(buffer: ArrayBuffer, signature: string, keyId: string): Promise<boolean | null> {
  const jwk = window.__GOVOPLAN_REMOTE_MODULE_KEYS__?.[keyId];
  if (!jwk) return null;
  const isOkp = jwk.kty === "OKP";
  const importAlgorithm = isOkp ? { name: "Ed25519" } : { name: "ECDSA", namedCurve: jwk.crv ?? "P-256" };
  const verifyAlgorithm = isOkp ? { name: "Ed25519" } : { name: "ECDSA", hash: "SHA-256" };
  const key = await crypto.subtle.importKey("jwk", jwk, importAlgorithm as AlgorithmIdentifier, false, ["verify"]);
  return crypto.subtle.verify(verifyAlgorithm as AlgorithmIdentifier, key, base64ToBuffer(signature), buffer);
}

function isPlatformWebModule(value: unknown): value is PlatformWebModule {
  if (!value || typeof value !== "object") return false;
  const item = value as Partial<PlatformWebModule>;
  return typeof item.id === "string" && typeof item.label === "string" && typeof item.version === "string";
}

function base64FromBuffer(buffer: ArrayBuffer): string {
  let binary = "";
  for (const byte of new Uint8Array(buffer)) binary += String.fromCharCode(byte);
  return btoa(binary);
}

function base64ToBuffer(value: string): ArrayBuffer {
  const normalized = value.trim().replace(/-/g, "+").replace(/_/g, "/");
  const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
  const binary = atob(padded);
  const buffer = new ArrayBuffer(binary.length);
  const bytes = new Uint8Array(buffer);
  for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
  return buffer;
}

function hexFromBuffer(buffer: ArrayBuffer): string {
  return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
}

function constantTimeEqual(left: string, right: string): boolean {
  if (left.length !== right.length) return false;
  let result = 0;
  for (let index = 0; index < left.length; index += 1) result |= left.charCodeAt(index) ^ right.charCodeAt(index);
  return result === 0;
}

export function installedLocalWebModules(): PlatformWebModule[] {
  return [...loadedLocalModules.values()];
}

export function uiCapability<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): T | null {
  return uiCapabilityForModules<T>(capabilityName, modules);
}

export function uiCapabilities<T = unknown>(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): T[] {
  return uiCapabilitiesForModules<T>(capabilityName, modules);
}

export function hasUiCapability(capabilityName: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
  return hasUiCapabilityForModules(capabilityName, modules);
}

export function moduleInstalled(moduleId: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
  return moduleInstalledForModules(moduleId, modules);
}

export function moduleIntegrationEnabled(moduleId: string, dependencyId: string, modules: PlatformWebModule[] = installedLocalWebModules()): boolean {
  return moduleIntegrationEnabledForModules(moduleId, dependencyId, modules);
}

export function routeContributionsForModules(modules: PlatformWebModule[]) {
  return routeContributionsForModuleList(modules);
}

export function publicRouteContributionsForModules(modules: PlatformWebModule[]) {
  return publicRouteContributionsForModuleList(modules);
}

export function dashboardWidgetsForModules(
  modules: PlatformWebModule[] = installedLocalWebModules(),
  projection?: EffectiveViewProjection | null
): DashboardWidgetContribution[] {
  const catalogue = viewSurfaceCatalogueForModules(modules);
  return uiCapabilitiesForModules<DashboardWidgetsUiCapability>("dashboard.widgets", modules).
  flatMap((capability) => capability.widgets ?? []).
  filter((widget) => isViewSurfaceVisible(projection, widget.surfaceId, catalogue)).
  sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}

export function navItemsForModules(
  modules: PlatformWebModule[],
  projection?: EffectiveViewProjection | null
): PlatformNavItem[] {
  const catalogue = viewSurfaceCatalogueForModules(modules);
  const moduleItems = modules.flatMap((module) =>
    (module.navItems ?? []).map((item) => ({
      ...item,
      surfaceId: item.surfaceId ?? navigationViewSurfaceId(module.id, item.to)
    }))
  );
  return [...shellNavItemsForModules(modules), ...moduleItems].
  map(resolveNavItemIcon).
  filter((item) => isViewSurfaceVisible(projection, item.surfaceId, catalogue)).
  sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
}

export function visibleNavItems(auth: AuthInfo | null | undefined, modules: PlatformWebModule[] = installedLocalWebModules(), projection?: EffectiveViewProjection | null): PlatformNavItem[] {
  return navItemsForModules(modules, projection).filter((item) => {
    if (item.allOf?.length && !item.allOf.every((scope) => hasScope(auth, scope))) return false;
    if (item.anyOf?.length && !hasAnyScope(auth, item.anyOf)) return false;
    return true;
  });
}

export function firstAccessibleRoute(auth: AuthInfo, modules: PlatformWebModule[] = installedLocalWebModules(), projection?: EffectiveViewProjection | null): string {
  return visibleNavItems(auth, modules, projection)[0]?.to ?? "/dashboard";
}
