export const FOUNDRY_OBJECTS = "application/x-vnd.palantir.rid.phonograph2-objects.object";
export const GOTHAM_OBJECTS = "application/x-vnd.palantir.rid.gotham.object";
export type SerializedTransfer = Record<string, string>;
export interface EnrichmentResult {
  dataTransfer: SerializedTransfer;
  errors: unknown[];
}
export type Enricher = (input: SerializedTransfer) => Promise<EnrichmentResult>;

function record(value: unknown, label: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new TypeError(`${label}: JSON 객체가 필요합니다.`);
  }
  return value as Record<string, unknown>;
}

export function parseIdentifiers(text: string): string[] {
  const value: unknown = JSON.parse(text);
  if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
    throw new TypeError("객체 식별자의 문자열 배열이 필요합니다.");
  }
  return value;
}

export function createEnricher(
  foundryBaseUrl: string,
  getAccessToken: () => Promise<string>,
): Enricher {
  const url = new URL(
    "/data-bank-service/api/data-transfer/batchEnrichDataTransfer",
    foundryBaseUrl,
  );
  return async (input) => {
    for (const [mediaType, serialized] of Object.entries(input)) {
      if (mediaType !== FOUNDRY_OBJECTS && mediaType !== GOTHAM_OBJECTS) {
        throw new Error(`지원하지 않는 보강 미디어 유형: ${mediaType}`);
      }
      parseIdentifiers(serialized);
    }
    const token = await getAccessToken();
    if (token.length === 0) throw new Error("접근 토큰을 받지 못했습니다.");
    const response = await fetch(url, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ dataTransfers: [input] }),
    });
    if (!response.ok) throw new Error(`Data Bank 요청 실패: HTTP ${response.status}`);
    const body: unknown = await response.json();
    const root = record(body, "응답");
    if (!Array.isArray(root.dataTransfers) || root.dataTransfers.length !== 1) {
      throw new TypeError("단일 입력에 대한 dataTransfers 결과가 필요합니다.");
    }
    const first = record(root.dataTransfers[0], "dataTransfers[0]");
    const values = record(first.dataTransfer, "dataTransfer");
    if (!Array.isArray(first.errors)) throw new TypeError("errors 배열이 필요합니다.");
    const dataTransfer: SerializedTransfer = Object.create(null);
    for (const [mediaType, serialized] of Object.entries(values)) {
      if (typeof serialized !== "string") {
        throw new TypeError(`문자열로 직렬화되지 않은 결과: ${mediaType}`);
      }
      dataTransfer[mediaType] = serialized;
    }
    return { dataTransfer, errors: first.errors };
  };
}

function synonym(result: EnrichmentResult, mediaType: string): string[] {
  if (result.errors.length !== 0) {
    throw new Error(`보강 오류: ${JSON.stringify(result.errors)}`);
  }
  const serialized = result.dataTransfer[mediaType];
  if (serialized === undefined) throw new Error(`동의 표현을 찾지 못했습니다: ${mediaType}`);
  return parseIdentifiers(serialized);
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function installEnrichedDragZone(
  element: HTMLElement,
  status: HTMLElement,
  enrich: Enricher,
) {
  let payload: SerializedTransfer | null = null;
  let version = 0;
  let disposed = false;
  element.draggable = false;

  const onDragStart = (event: DragEvent) => {
    if (payload === null || event.dataTransfer === null) {
      event.preventDefault();
      return;
    }
    for (const [mediaType, serialized] of Object.entries(payload)) {
      event.dataTransfer.setData(mediaType, serialized);
    }
    event.dataTransfer.effectAllowed = "move";
  };
  element.addEventListener("dragstart", onDragStart);

  return {
    async update(gothamObjectIds: readonly string[]): Promise<boolean> {
      if (disposed) throw new Error("해제한 drag zone은 갱신할 수 없습니다.");
      const current = ++version;
      payload = null;
      element.draggable = false;
      if (gothamObjectIds.length === 0) {
        status.textContent = "드래그할 객체를 선택하세요.";
        return false;
      }
      status.textContent = "Foundry 동의 표현을 찾는 중입니다.";
      try {
        const original = JSON.stringify(gothamObjectIds);
        const result = await enrich({ [GOTHAM_OBJECTS]: original });
        const foundryIds = synonym(result, FOUNDRY_OBJECTS);
        if (foundryIds.length === 0) throw new Error("연결된 Foundry 객체가 없습니다.");
        if (current !== version) return false;
        payload = {
          [GOTHAM_OBJECTS]: original,
          [FOUNDRY_OBJECTS]: JSON.stringify(foundryIds),
        };
        element.draggable = true;
        status.textContent = "Gotham과 Foundry 표현을 드래그할 수 있습니다.";
        return true;
      } catch (error) {
        if (current === version) status.textContent = errorMessage(error);
        return false;
      }
    },
    dispose() {
      disposed = true;
      version += 1;
      payload = null;
      element.draggable = false;
      element.removeEventListener("dragstart", onDragStart);
    },
  };
}

export function installGothamDropZone(
  element: HTMLElement,
  status: HTMLElement,
  list: HTMLElement,
  enrich: Enricher,
): () => void {
  let version = 0;
  const allowDrop = (event: DragEvent) => {
    const types = event.dataTransfer?.types ?? [];
    if (types.includes(GOTHAM_OBJECTS) || types.includes(FOUNDRY_OBJECTS)) {
      event.preventDefault();
    }
  };
  const onDrop = async (event: DragEvent) => {
    event.preventDefault();
    const current = ++version;
    list.replaceChildren();
    element.setAttribute("aria-busy", "true");
    status.textContent = "드롭한 데이터를 처리하는 중입니다.";
    try {
      if (event.dataTransfer === null) throw new Error("DataTransfer가 없습니다.");
      const gothamData = event.dataTransfer.getData(GOTHAM_OBJECTS);
      const foundryData = event.dataTransfer.getData(FOUNDRY_OBJECTS);
      let identifiers: string[];
      if (gothamData !== "") {
        identifiers = parseIdentifiers(gothamData);
      } else if (foundryData !== "") {
        const foundryIds = parseIdentifiers(foundryData);
        if (foundryIds.length === 0) {
          identifiers = [];
        } else {
          const result = await enrich({ [FOUNDRY_OBJECTS]: foundryData });
          identifiers = synonym(result, GOTHAM_OBJECTS);
          if (identifiers.length === 0) throw new Error("연결된 Gotham 객체가 없습니다.");
        }
      } else {
        throw new Error("Foundry 또는 Gotham 객체 식별자를 받지 못했습니다.");
      }
      if (current !== version) return;
      const fragment = document.createDocumentFragment();
      for (const identifier of identifiers) {
        const item = document.createElement("li");
        item.textContent = identifier;
        fragment.appendChild(item);
      }
      list.replaceChildren(fragment);
      status.textContent = `${identifiers.length}개 Gotham 식별자를 받았습니다.`;
    } catch (error) {
      if (current === version) status.textContent = errorMessage(error);
    } finally {
      if (current === version) element.setAttribute("aria-busy", "false");
    }
  };
  element.addEventListener("dragenter", allowDrop);
  element.addEventListener("dragover", allowDrop);
  element.addEventListener("drop", onDrop);
  return () => {
    version += 1;
    element.removeEventListener("dragenter", allowDrop);
    element.removeEventListener("dragover", allowDrop);
    element.removeEventListener("drop", onDrop);
    element.setAttribute("aria-busy", "false");
  };
}
