import type { WidgetConfigResponse } from "./public-contracts.js";

import { loadAccessibilityCapabilities } from "./accessibility-loader.js";
import type { AccessibilityCapabilities } from "./accessibility/types.js";
import { createWidgetApiClient, type WidgetApiClient } from "./api.js";
import {
  EMBED_TAG_NAME,
  INLINE_TAG_NAME,
  registerEmbedElement,
  registerInlineElement,
} from "./register.js";
import { getOrCreateSessionId } from "./session.js";
import {
  isAkintuSurfaceElement,
  type AkintuSurfaceConfiguration,
} from "./surface-element.js";

export interface InitAkintuOptions {
  apiKey: string;
  apiBaseUrl?: string | undefined;
  fetchImplementation?: typeof fetch | undefined;
  root?: ParentNode;
}

export interface InitAkintuResult {
  configuredEmbeds: number;
  mountedInlines: number;
}

interface LoadedRuntime {
  accessibility: AccessibilityCapabilities;
  apiClient: WidgetApiClient;
  config: WidgetConfigResponse;
  configuredElements: WeakSet<Element>;
  sessionId: string;
}

type AccessibilityLoader = typeof loadAccessibilityCapabilities;

// A document gets one configuration request and one in-memory credential
// closure. Repeated loader calls can discover late React mounts without
// duplicating already configured elements.
const runtimeByDocument = new WeakMap<
  Document,
  Promise<LoadedRuntime | undefined>
>();
let accessibilityLoader: AccessibilityLoader = loadAccessibilityCapabilities;

/**
 * Loads tenant configuration and activates only its enabled page surfaces.
 * Configuration failure is intentionally represented as an empty result so a
 * host page never receives a partially initialized widget.
 */
export async function initAkintu(
  options: InitAkintuOptions,
): Promise<InitAkintuResult> {
  const root = options.root ?? document;
  const ownerDocument = getOwnerDocument(root);
  if (options.apiKey.trim().length === 0) return emptyResult();

  let runtimePromise = runtimeByDocument.get(ownerDocument);
  if (runtimePromise === undefined) {
    runtimePromise = loadRuntime(options);
    runtimeByDocument.set(ownerDocument, runtimePromise);
  }
  const runtime = await runtimePromise;
  if (runtime === undefined) return emptyResult();
  return mountConfiguredSurfaces(root, ownerDocument, runtime);
}

/**
 * Builds the shared per-page runtime. The API key remains reachable only from
 * the API client closure captured here.
 */
async function loadRuntime(
  options: InitAkintuOptions,
): Promise<LoadedRuntime | undefined> {
  const apiClient = createWidgetApiClient({
    apiKey: options.apiKey,
    apiBaseUrl: options.apiBaseUrl,
    fetchImplementation: options.fetchImplementation,
  });

  try {
    const config = await apiClient.getConfig();
    const accessibility = await getAccessibility(config.accessibility);
    return {
      accessibility,
      apiClient,
      config,
      configuredElements: new WeakSet<Element>(),
      sessionId: getOrCreateSessionId({
        sessionVersion: config.sessionVersion,
      }),
    };
  } catch {
    // Configuration is a fail-closed boundary: no surface mounts on failure.
    return undefined;
  }
}

async function getAccessibility(
  flags: WidgetConfigResponse["accessibility"],
): Promise<AccessibilityCapabilities> {
  // Avoid even evaluating import() when voice support is disabled, keeping the
  // accessibility modules out of the browser's network graph.
  if (!flags.stt && !flags.tts) return {};
  try {
    return await accessibilityLoader(flags);
  } catch {
    // Voice features are enhancements and must never block text interaction.
    return {};
  }
}

function mountConfiguredSurfaces(
  root: ParentNode,
  ownerDocument: Document,
  runtime: LoadedRuntime,
): InitAkintuResult {
  const registry = ownerDocument.defaultView?.customElements;
  if (registry === undefined) return emptyResult();

  let configuredEmbeds = 0;
  let mountedInlines = 0;
  // Custom elements are registered only after their remote feature is known to
  // be enabled. Disabled markup therefore stays inert by contract.
  if (runtime.config.embedEnabled) {
    registerEmbedElement(registry);
    for (const element of findElements(root, EMBED_TAG_NAME)) {
      if (configureElement(element, runtime)) configuredEmbeds += 1;
    }
  }
  if (runtime.config.inlineEnabled) {
    registerInlineElement(registry);
    for (const target of findElements(root, "[data-akintu-context]")) {
      if (mountInline(target, ownerDocument, runtime)) mountedInlines += 1;
    }
  }
  return { configuredEmbeds, mountedInlines };
}

function configureElement(
  element: Element,
  runtime: LoadedRuntime,
  context?: string,
): boolean {
  if (
    runtime.configuredElements.has(element) ||
    !isAkintuSurfaceElement(element)
  ) {
    return false;
  }
  const configuration: AkintuSurfaceConfiguration = {
    // Passing the client object keeps the key out of element attributes and
    // host-visible markup.
    apiClient: runtime.apiClient,
    accessibility: runtime.accessibility,
    sessionId: runtime.sessionId,
    sessionVersion: runtime.config.sessionVersion,
    theme: runtime.config.theme,
  };
  if (context !== undefined) configuration.context = context;
  element.configure(configuration);
  runtime.configuredElements.add(element);
  return true;
}

function mountInline(
  target: Element,
  ownerDocument: Document,
  runtime: LoadedRuntime,
): boolean {
  // Context comes from untrusted host markup and must satisfy the same length
  // boundary as ChatRequest before it can be sent to the API.
  const context = target.getAttribute("data-akintu-context")?.trim();
  if (context === undefined || context.length < 1 || context.length > 128) {
    return false;
  }
  // A direct-child lookup makes repeated initialization idempotent without
  // confusing nested contextual sections with this target's own surface.
  const existingInline = Array.from(target.children).find(
    (child) => child.localName === INLINE_TAG_NAME,
  );
  if (existingInline !== undefined) {
    return configureElement(existingInline, runtime, context);
  }
  const inline = ownerDocument.createElement(INLINE_TAG_NAME);
  target.append(inline);
  configureElement(inline, runtime, context);
  return true;
}

function findElements(root: ParentNode, selector: string): Element[] {
  const matches: Element[] = [];
  if (root instanceof Element && root.matches(selector)) matches.push(root);
  matches.push(...root.querySelectorAll(selector));
  return matches;
}

function getOwnerDocument(root: ParentNode): Document {
  return root instanceof Document ? root : (root.ownerDocument ?? document);
}

function emptyResult(): InitAkintuResult {
  return { configuredEmbeds: 0, mountedInlines: 0 };
}

export function setAccessibilityLoaderForTests(
  loader: AccessibilityLoader,
): void {
  accessibilityLoader = loader;
}

export function resetLoaderForTests(ownerDocument: Document): void {
  runtimeByDocument.delete(ownerDocument);
  accessibilityLoader = loadAccessibilityCapabilities;
}
