const NATIVE_HOST_NAME = "com.willett.murtco_drag";
const ALLOWED_CONTENT_MESSAGES = new Set([
  "prepareBegin",
  "prepareChunk",
  "prepareEnd",
  "armOverlay",
  "disarmOverlay",
  "discard",
  "clientLog",
  "ping"
]);

const MESSAGE_FIELDS = {
  prepareBegin: [
    "type", "transferId", "attachmentId", "filename", "mimeType", "byteLength"
  ],
  prepareChunk: ["type", "transferId", "sequence", "base64"],
  prepareEnd: ["type", "transferId", "sha256"],
  armOverlay: ["type", "token"],
  disarmOverlay: ["type", "token"],
  discard: ["type", "token"],
  clientLog: ["type", "event", "state", "reason"],
  ping: ["type", "requestId"]
};

export function createNativePortRouter({ connectNative, now = () => performance.now() }) {
  if (typeof connectNative !== "function") {
    throw new TypeError("connectNative must be a function.");
  }

  const contentPorts = new Set();
  const transferOwners = new Map();
  const tokenOwners = new Map();
  const pingOwners = [];
  let nativePort = null;

  function postToContent(port, message) {
    if (!contentPorts.has(port)) {
      return false;
    }
    try {
      port.postMessage(message);
      return true;
    } catch {
      detach(port);
      return false;
    }
  }

  function postError(port, code, message, identifiers = {}) {
    postToContent(port, {
      type: "error",
      code,
      message,
      ...identifiers,
      routerTimeMs: now()
    });
  }

  function clearNativeOwners() {
    transferOwners.clear();
    tokenOwners.clear();
    pingOwners.length = 0;
  }

  function notifyNativeReset(intentional) {
    for (const port of [...contentPorts]) {
      if (intentional) {
        postToContent(port, { type: "nativeReset" });
      } else {
        postError(
          port,
          "native_disconnected",
          "The MurtCo native drag helper disconnected."
        );
      }
    }
  }

  function projectMessage(message) {
    const fields = MESSAGE_FIELDS[message.type] || [];
    return Object.fromEntries(
      fields
        .filter((field) => message[field] !== undefined)
        .map((field) => [field, message[field]])
    );
  }

  function ownerForNativeMessage(message) {
    if (message.transferId) {
      return transferOwners.get(message.transferId) || null;
    }
    if (message.token) {
      return tokenOwners.get(message.token) || null;
    }
    if (message.type === "pong") {
      return pingOwners.shift() || null;
    }
    return null;
  }

  function handleNativeMessage(message) {
    if (!message || typeof message !== "object") {
      return;
    }
    const owner = ownerForNativeMessage(message);
    if (!owner || !contentPorts.has(owner)) {
      return;
    }

    if (message.type === "prepared" && message.transferId && message.token) {
      transferOwners.delete(message.transferId);
      tokenOwners.set(message.token, owner);
    } else if (message.type === "error" && message.transferId) {
      transferOwners.delete(message.transferId);
    } else if (
      [
        "discarded",
        "dragFailed"
      ].includes(message.type) &&
      message.token
    ) {
      tokenOwners.delete(message.token);
    }
    postToContent(owner, message);
  }

  function handleNativeDisconnect(disconnectedPort) {
    if (disconnectedPort !== nativePort) {
      return;
    }
    nativePort = null;
    clearNativeOwners();
    notifyNativeReset(false);
  }

  function ensureNativePort(contentPort) {
    if (nativePort) {
      return nativePort;
    }
    try {
      const connectedPort = connectNative(NATIVE_HOST_NAME);
      nativePort = connectedPort;
      connectedPort.onMessage.addListener(handleNativeMessage);
      connectedPort.onDisconnect.addListener(() => {
        handleNativeDisconnect(connectedPort);
      });
      return connectedPort;
    } catch {
      postError(
        contentPort,
        "native_unavailable",
        "The MurtCo native drag helper is not installed or registered."
      );
      return null;
    }
  }

  function handleContentMessage(contentPort, message) {
    if (!message || !ALLOWED_CONTENT_MESSAGES.has(message.type)) {
      postError(
        contentPort,
        "unsupported_message",
        "The requested native drag operation is not supported."
      );
      return;
    }

    const hostPort = ensureNativePort(contentPort);
    if (!hostPort) {
      return;
    }

    if (message.type.startsWith("prepare")) {
      if (typeof message.transferId !== "string" || !message.transferId) {
        postError(contentPort, "invalid_transfer", "A transfer ID is required.");
        return;
      }
      const owner = transferOwners.get(message.transferId);
      if (owner && owner !== contentPort) {
        postError(contentPort, "transfer_conflict", "The transfer ID is already in use.");
        return;
      }
      if (!owner && message.type !== "prepareBegin") {
        postError(contentPort, "unknown_transfer", "The transfer has not started.");
        return;
      }
      transferOwners.set(message.transferId, contentPort);
    } else if (
      message.type === "armOverlay" ||
      message.type === "disarmOverlay" ||
      message.type === "discard"
    ) {
      if (typeof message.token !== "string" || tokenOwners.get(message.token) !== contentPort) {
        postError(contentPort, "unknown_token", "The prepared PDF token is unavailable.");
        return;
      }
    } else if (message.type === "ping") {
      pingOwners.push(contentPort);
    }

    try {
      hostPort.postMessage(projectMessage(message));
    } catch {
      if (hostPort === nativePort) {
        handleNativeDisconnect(hostPort);
      }
    }
  }

  function detach(contentPort) {
    contentPorts.delete(contentPort);
    for (const [transferId, owner] of transferOwners) {
      if (owner === contentPort) {
        transferOwners.delete(transferId);
      }
    }
    for (const [token, owner] of tokenOwners) {
      if (owner === contentPort) {
        tokenOwners.delete(token);
      }
    }
    for (let index = pingOwners.length - 1; index >= 0; index -= 1) {
      if (pingOwners[index] === contentPort) {
        pingOwners.splice(index, 1);
      }
    }
  }

  return {
    attach(contentPort) {
      contentPorts.add(contentPort);
      contentPort.onMessage.addListener((message) => {
        handleContentMessage(contentPort, message);
      });
      contentPort.onDisconnect.addListener(() => {
        detach(contentPort);
      });
    }
  };
}
