import { LitElement, noChange } from "lit";
import { customElement, property } from "lit/decorators.js";
import { fixRenderedAnchorLinks } from "~/utils/markdown-utils";
import { marked } from "marked";
import Slugger from "github-slugger";
import { scheduleHighlight } from "~/utils/highlighter";

// Styles
import FontStyles from "~/styles/font-styles";
import InputStyles from "~/styles/input-styles";
import FlexStyles from "~/styles/flex-styles";
import AuthStyles from "~/styles/auth-styles";
import ParamListStyles from "~/styles/param-list-styles";
import EndpointStyles from "~/styles/endpoint-styles";
import MicrolighterStyles from "~/styles/microlighter-styles";
import TabStyles from "~/styles/tab-styles";
import NavStyles from "~/styles/nav-styles";
import InfoStyles from "~/styles/info-styles";
import CustomStyles from "~/styles/custom-styles";
import DialogBoxStyles from "~/styles/dialog-box-styles";
import LayoutStyles from "~/styles/layout-styles";
import ThemeTokens, {
  DEFAULT_THEME,
  applyCustomTheme,
  isValidHexColor,
  normalizeTheme,
} from "~/styles/theme-tokens";

import {
  advancedSearch,
  getMatchedPaths,
  getMatchedComponents,
  rapidocApiKey,
  sleep,
} from "~/utils/common-utils";
import ProcessSpec from "~/utils/spec-parser";
import { enableMockServer, disableMockServer, updateMockConfig } from "~/utils/mock-interceptor";
import mainBodyTemplate from "~/templates/main-body-template";
import { expandParentNavContainers } from "~/templates/navbar-template";
import { applyApiKey, onClearAllApiKeys } from "~/templates/security-scheme-template";
import { setApiServer } from "~/templates/server-template";
import {
  generateMcpServer as mcpServerGenerator,
  buildMcpManifest,
  estimateTokenCost,
  parseMcpEnabled,
  createZip,
} from "~/utils/mcp-generator";
import { bindNavBar } from "~/utils/bind-navbar";
import { methodPathSlug } from "~/utils/slugger";
import { AutoColorSchemeController } from "~/utils/auto-color-scheme";

const paramsConverter = {
  fromAttribute: (attr) => {
    if (!attr) {
      return null;
    }
    const trimmed = attr.trim();
    if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
      try {
        return JSON.parse(trimmed);
      } catch {
        return trimmed;
      }
    }
    return trimmed;
  },
  toAttribute: (prop) => {
    if (typeof prop === "object" && prop !== null) {
      return JSON.stringify(prop);
    }
    return prop;
  },
};

// Input that marks the reader's first interaction with the page (see getContentScrollBehavior)
const INTERACTION_EVENTS = ["pointerdown", "keydown", "wheel", "touchstart"];

@customElement("rapi-doc")
export default class RapiDoc extends LitElement {
  constructor() {
    super();
    this.showSummaryWhenCollapsed = true;
    this._intersectingElements = new Map();
    // Will activate intersection observer only after spec load and hash analyze
    // to scroll to the proper element without being reverted by observer behavior
    this.isIntersectionObserverActive = false;
    this._userSetLayout = false;
    this._isDefaultingLayout = false;
    this._observerLockTimeout = null;
    this._observerLockCleanup = null;
    // Set on the reader's first pointer, key, wheel or touch input (see getContentScrollBehavior)
    this._readerHasInteracted = false;
    this._onFirstInteraction = null;
    this._onHashChange = null;
    // Spec loads carry a token; only the latest load may apply its result (see loadSpec)
    this._specLoadSeq = 0;
    // The spec-url attribute value whose load has been handled, or that an inlined spec superseded
    this._lastSpecUrlRequested = null;
    this.initIntersectionObserver();
  }

  // Heading
  @property({ type: String, attribute: "heading-text" }) headingText = "";
  @property({ type: String, attribute: "goto-path" }) gotoPath = "";

  // Spec
  @property({ type: String, attribute: "update-route" }) updateRoute = "true";
  @property({ type: String, attribute: "spec-url" }) specUrl = "";
  @property({ type: String, attribute: "sort-tags" }) sortTags = "false";
  @property({ type: String, attribute: "sort-schemas" }) sortSchemas = "false";
  @property({ type: String, attribute: "generate-missing-tags" }) generateMissingTags = "false";
  @property({ type: String, attribute: "show-unused-tags" }) showUnusedTags = "false";
  @property({ type: String, attribute: "sort-endpoints-by" }) sortEndpointsBy = "path";
  @property({ type: String, attribute: false }) specFile: any;

  // UI Layouts
  @property({ type: String, reflect: true }) layout = "row";
  @property({ type: String, attribute: "render-style" }) renderStyle = "read";
  @property({ type: String, attribute: "default-schema-tab" }) defaultSchemaTab = "example";
  @property({ type: String, attribute: "response-area-height" }) responseAreaHeight = "400px";
  @property({ type: String, attribute: "fill-request-fields-with-example" })
  fillRequestFieldsWithExample = "true";
  @property({ type: String, attribute: "persist-auth" }) persistAuth = "false";

  // Schema Styles
  @property({ type: String, attribute: "schema-style" }) schemaStyle = "tree";
  @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999;
  @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded =
    "false";
  @property({ type: String, attribute: "schema-hide-read-only" }) schemaHideReadOnly = "true";
  @property({ type: String, attribute: "schema-hide-write-only" }) schemaHideWriteOnly = "true";

  // API Server
  @property({ type: String, attribute: "api-key-name" }) apiKeyName = "";
  @property({ type: String, attribute: "api-key-location" }) apiKeyLocation = "header";
  @property({ type: String, attribute: "api-key-value" }) apiKeyValue = "-";
  @property({ type: String, attribute: "default-api-server" }) defaultApiServerUrl = "";
  @property({ type: String, attribute: "server-url" }) serverUrl = "";
  @property({ type: String, attribute: "oauth-receiver" }) oauthReceiver = "oauth-receiver.html";
  @property({ type: Object, attribute: "additional-authorize-params", converter: paramsConverter })
  additionalAuthorizeParams: any;
  @property({ type: Object, attribute: "additional-token-params", converter: paramsConverter })
  additionalTokenParams: any;

  // Mock Server
  @property({ type: String, attribute: "mock-server" }) mockServer = "false";
  // A string of codes for every operation, or a map by operation (`{ "POST /orders": "503, 200", "*": "200" }`)
  @property({ type: String, attribute: "mock-server-status-code" }) mockServerStatusCode:
    | string
    | Record<string, string> = "200";
  @property({ type: String, attribute: "mock-server-status-strategy" }) mockServerStatusStrategy =
    "example";
  @property({ type: Number, attribute: "mock-server-delay" }) mockServerDelay = 0;
  @property({ type: String, attribute: "mock-server-log" }) mockServerLog = "false";

  // Hide/Show Sections & Enable Disable actions
  @property({ type: String, attribute: "show-header" }) showHeader = "true";
  @property({ type: String, attribute: "show-side-nav" }) showSideNav = "true";
  @property({ type: String, attribute: "show-info" }) showInfo = "true";
  @property({ type: String, attribute: "allow-authentication" }) allowAuthentication = "true";
  @property({ type: String, attribute: "allow-try" }) allowTry = "true";
  @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false";
  @property({ type: String, attribute: "allow-spec-url-load" }) allowSpecUrlLoad = "true";
  @property({ type: String, attribute: "allow-spec-file-load" }) allowSpecFileLoad = "true";
  @property({ type: String, attribute: "allow-spec-file-download" }) allowSpecFileDownload =
    "false";
  @property({ type: String, attribute: "allow-search" }) allowSearch = "true";
  @property({ type: String, attribute: "allow-advanced-search" }) allowAdvancedSearch = "true";
  @property({ type: String, attribute: "allow-server-selection" }) allowServerSelection = "true";
  @property({ type: String, attribute: "allow-schema-description-expand-toggle" })
  allowSchemaDescriptionExpandToggle = "true";
  // "true" shows the Export MCP Server button; "none" or "false" hides it
  @property({ type: String, attribute: "mcp" }) mcp = "false";
  @property({ type: String, attribute: "mcp-server-name" }) mcpServerName = "";
  @property({ type: String, attribute: "show-components" }) showComponents = "false";
  @property({ type: String, attribute: "page-direction" }) pageDirection = "ltr";
  @property({ type: String, attribute: "scroll-behavior" }) scrollBehavior = "smooth";
  @property({ type: String, attribute: "auto-height" }) autoHeight = "false";
  @property({ type: String, attribute: "scroll-container" }) scrollContainer = "";
  @property({ type: String, attribute: "scroll-offset" }) scrollOffset = "";

  // Main Colors and Font
  @property({ type: String, reflect: true }) theme = DEFAULT_THEME;
  @property({ type: String, attribute: "primary-color" }) primaryColor = "";
  @property({ type: String, attribute: "color-scheme", reflect: true }) colorScheme = "dark";
  // color-scheme="auto": follows the page's theme (see utils/auto-color-scheme.js)
  _autoColorScheme = new AutoColorSchemeController(this);
  @property({ type: String, reflect: true }) radius = "sm";
  @property({ type: String, reflect: true }) scale = "default";
  @property({ type: String, attribute: "css-file" }) cssFile = "";
  @property({ type: String, attribute: "css-classes" }) cssClasses = "";

  // Nav Bar
  @property({ type: String, attribute: "nav-item-spacing", reflect: true }) navItemSpacing =
    "default";
  @property({ type: String, attribute: "nav-item-overflow", reflect: true }) navItemOverflow =
    "ellipsis";
  @property({ type: String, attribute: "show-method-in-nav-bar" }) showMethodInNavBar = "false";
  @property({ type: String, attribute: "use-path-in-nav-bar" }) usePathInNavBar = "false";
  @property({ type: String, attribute: "info-description-headings-in-navbar" })
  infoDescriptionHeadingsInNavBar = "false";

  // Fetch Options
  @property({ type: String, attribute: "fetch-credentials" }) fetchCredentials = "";

  // Filters
  @property({ type: String, attribute: "match-paths" }) matchPaths = "";
  @property({ type: String, attribute: "match-type" }) matchType = "includes";
  @property({ type: String, attribute: "remove-endpoints-with-badge-label-as" })
  removeEndpointsWithBadgeLabelAs = "";

  // Internal Properties
  @property({ type: Boolean }) loading = false;
  @property({ type: String }) focusedElementId = "";
  @property({ type: Object }) advancedSearchMatches: any;
  @property({ type: String }) searchVal = "";

  static get styles() {
    return [
      ThemeTokens,
      FontStyles,
      InputStyles,
      FlexStyles,
      AuthStyles,
      ParamListStyles,
      EndpointStyles,
      MicrolighterStyles,
      TabStyles,
      NavStyles,
      InfoStyles,
      DialogBoxStyles,
      LayoutStyles,
      CustomStyles,
    ];
  }

  // Startup
  connectedCallback() {
    super.connectedCallback();
    this.initIntersectionObserver();
    if (this.shadowRoot && this.shadowRoot.querySelector(".main-content")) {
      this._hasDsd = true;
    }
    const parent = this.parentElement;
    if (parent && parent.tagName === "BODY") {
      if (parent.offsetWidth === 0 && parent.style.width === "") {
        parent.style.width = "100vw";
      }
      if (parent.offsetHeight === 0 && parent.style.height === "") {
        parent.style.height = "100vh";
      }
      if (!parent.style.marginTop) {
        parent.style.marginTop = "0";
      }
      if (!parent.style.marginRight) {
        parent.style.marginRight = "0";
      }
      if (!parent.style.marginBottom) {
        parent.style.marginBottom = "0";
      }
      if (!parent.style.marginLeft) {
        parent.style.marginLeft = "0";
      }
    }

    if (!this.renderStyle || !"read, view, focused,".includes(`${this.renderStyle},`)) {
      this.renderStyle = "focused";
    }

    if (this.hasAttribute("layout")) {
      const attrLayout = this.getAttribute("layout");
      if ("row, column,".includes(`${attrLayout},`)) {
        this.layout = attrLayout;
        this._userSetLayout = true;
      }
    }

    if (!this._userSetLayout) {
      const defaultLayout =
        this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
      this._isDefaultingLayout = true;
      this.layout = defaultLayout;
      this.setAttribute("layout", defaultLayout);
      this._isDefaultingLayout = false;
    } else if (!this.layout || !"row, column,".includes(`${this.layout},`)) {
      this.layout =
        this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
      this.setAttribute("layout", this.layout);
    }
    if (!this.schemaStyle || !"tree, table,".includes(`${this.schemaStyle},`)) {
      this.schemaStyle = "tree";
    }
    if (this.theme === "light" || this.theme === "dark") {
      this.colorScheme = this.theme;
      this.theme = DEFAULT_THEME;
    }
    if (this.primaryColor && isValidHexColor(this.primaryColor)) {
      this.theme = this.primaryColor;
    }
    const normalizedTheme = normalizeTheme(this.theme);
    this.theme = normalizedTheme.theme;
    this.setAttribute("theme", this.theme);
    if (!this.colorScheme || !"dark, light, system,".includes(`${this.colorScheme},`)) {
      this.colorScheme = "dark";
    }
    if (!this.radius || !"none, sm, md, lg, full,".includes(`${this.radius},`)) {
      this.radius = "sm";
    }
    if (!this.scale || !"sm, default, lg,".includes(`${this.scale},`)) {
      this.scale = "default";
    }
    if (!this.navItemSpacing || !"compact, default, relaxed,".includes(`${this.navItemSpacing},`)) {
      this.navItemSpacing = "default";
    }
    if (!this.getAttribute("theme")) {
      this.setAttribute("theme", this.theme);
    }
    if (!this.getAttribute("color-scheme")) {
      this.setAttribute("color-scheme", this.colorScheme);
    }
    if (!this.getAttribute("radius")) {
      this.setAttribute("radius", this.radius);
    }
    if (!this.getAttribute("scale")) {
      this.setAttribute("scale", this.scale);
    }
    applyCustomTheme(this, this.theme);
    if (
      !this.defaultSchemaTab ||
      !"example, schema, model,".includes(`${this.defaultSchemaTab},`)
    ) {
      this.defaultSchemaTab = "example";
    } else if (this.defaultSchemaTab === "model") {
      this.defaultSchemaTab = "schema";
    }
    if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) {
      this.schemaExpandLevel = 99999;
    }
    if (
      !this.schemaDescriptionExpanded ||
      !"true, false,".includes(`${this.schemaDescriptionExpanded},`)
    ) {
      this.schemaDescriptionExpanded = "false";
    }

    if (!this.schemaHideReadOnly || !"default, never,".includes(`${this.schemaHideReadOnly},`)) {
      this.schemaHideReadOnly = "default";
    }
    if (!this.schemaHideWriteOnly || !"default, never,".includes(`${this.schemaHideWriteOnly},`)) {
      this.schemaHideWriteOnly = "default";
    }

    if (
      !this.fillRequestFieldsWithExample ||
      !"true, false,".includes(`${this.fillRequestFieldsWithExample},`)
    ) {
      this.fillRequestFieldsWithExample = "true";
    }
    if (!this.persistAuth || !"true, false,".includes(`${this.persistAuth},`)) {
      this.persistAuth = "false";
    }
    if (!this.responseAreaHeight) {
      this.responseAreaHeight = "400px";
    }

    if (!this.allowSearch || !"true, false,".includes(`${this.allowSearch},`)) {
      this.allowSearch = "true";
    }
    if (!this.allowAdvancedSearch || !"true, false,".includes(`${this.allowAdvancedSearch},`)) {
      this.allowAdvancedSearch = "true";
    }

    if (!this.allowTry || !"true, false,".includes(`${this.allowTry},`)) {
      this.allowTry = "true";
    }
    if (!this.apiKeyValue) {
      this.apiKeyValue = "-";
    }
    if (!this.apiKeyLocation) {
      this.apiKeyLocation = "header";
    }
    if (!this.apiKeyName) {
      this.apiKeyName = "";
    }

    if (!this.oauthReceiver) {
      this.oauthReceiver = "oauth-receiver.html";
    }
    if (!this.updateRoute || !"true, false,".includes(`${this.updateRoute},`)) {
      this.updateRoute = "true";
    }
    if (!this.sortTags || !"true, false,".includes(`${this.sortTags},`)) {
      this.sortTags = "false";
    }
    if (!this.sortSchemas || !"true, false,".includes(`${this.sortSchemas},`)) {
      this.sortSchemas = "false";
    }
    if (!this.generateMissingTags || !"true, false,".includes(`${this.generateMissingTags},`)) {
      this.generateMissingTags = "false";
    }
    if (!this.showUnusedTags || !"true, false,".includes(`${this.showUnusedTags},`)) {
      this.showUnusedTags = "false";
    }
    if (
      !this.sortEndpointsBy ||
      !"method, path, summary, none,".includes(`${this.sortEndpointsBy},`)
    ) {
      this.sortEndpointsBy = "path";
    }

    if (!this.navItemSpacing || !"compact, relaxed, default,".includes(`${this.navItemSpacing},`)) {
      this.navItemSpacing = "default";
    }
    if (
      !this.showMethodInNavBar ||
      !"false, as-plain-text, as-colored-text, as-colored-block,".includes(
        `${this.showMethodInNavBar},`,
      )
    ) {
      this.showMethodInNavBar = "false";
    }
    if (!this.usePathInNavBar || !"true, false,".includes(`${this.usePathInNavBar},`)) {
      this.usePathInNavBar = "false";
    }
    if (!this.navItemOverflow || !"ellipsis, wrap,".includes(`${this.navItemOverflow},`)) {
      this.navItemOverflow = "ellipsis";
    }
    if (!this.getAttribute("nav-item-overflow")) {
      this.setAttribute("nav-item-overflow", this.navItemOverflow);
    }
    if (!this.showInfo || !"true, false,".includes(`${this.showInfo},`)) {
      this.showInfo = "true";
    }
    if (!this.allowServerSelection || !"true, false,".includes(`${this.allowServerSelection},`)) {
      this.allowServerSelection = "true";
    }
    if (!this.allowAuthentication || !"true, false,".includes(`${this.allowAuthentication},`)) {
      this.allowAuthentication = "true";
    }
    if (
      !this.allowSchemaDescriptionExpandToggle ||
      !"true, false,".includes(`${this.allowSchemaDescriptionExpandToggle},`)
    ) {
      this.allowSchemaDescriptionExpandToggle = "true";
    }

    if (!this.showSideNav || !"true false".includes(this.showSideNav)) {
      this.showSideNav = "true";
    }
    if (!this.showComponents || !"true false".includes(this.showComponents)) {
      this.showComponents = "false";
    }
    if (
      !this.infoDescriptionHeadingsInNavBar ||
      !"true, false,".includes(`${this.infoDescriptionHeadingsInNavBar},`)
    ) {
      this.infoDescriptionHeadingsInNavBar = "false";
    }
    if (
      !this.fetchCredentials ||
      !"omit, same-origin, include,".includes(`${this.fetchCredentials},`)
    ) {
      this.fetchCredentials = "";
    }
    if (!this.scrollBehavior || !"smooth, auto,".includes(`${this.scrollBehavior},`)) {
      this.scrollBehavior = "auto";
    }

    if (!this.matchType || !"includes regex".includes(this.matchType)) {
      this.matchType = "includes";
    }
    if (!this.matchPaths) {
      this.matchPaths = "";
    }
    if (!this.removeEndpointsWithBadgeLabelAs) {
      this.removeEndpointsWithBadgeLabelAs = "";
    }
    if (!this.cssFile) {
      this.cssFile = null;
    }
    if (!this.cssClasses) {
      this.cssClasses = "";
    }
    if (!this.mcp) {
      this.mcp = "none";
    }

    if (!this._onHashChange) {
      this._onHashChange = () => {
        this.scrollToPath(this.getElementIDFromURL());
      };
      window.addEventListener("hashchange", this._onHashChange, true);
    }

    if (!this._readerHasInteracted && !this._onFirstInteraction) {
      this._onFirstInteraction = () => {
        this._readerHasInteracted = true;
        this.removeFirstInteractionListeners();
      };
      INTERACTION_EVENTS.forEach((type) =>
        window.addEventListener(type, this._onFirstInteraction, { capture: true, passive: true }),
      );
    }

    // Auto-hydrate from inlined spec or spec-url attribute if not already loaded. This block owns
    // the initial load; the deferred spec-url path in attributeChangedCallback skips the value
    // recorded in _lastSpecUrlRequested so one <rapi-doc spec-url> loads once.
    if (!this.resolvedSpec) {
      const inlinedSpecEl = this.querySelector(
        'script[type="application/json"][slot="rapidoc-spec"]',
      );
      if (inlinedSpecEl?.textContent) {
        try {
          const parsed = JSON.parse(inlinedSpecEl.textContent);
          // The inlined spec wins over a spec-url attribute present at connect
          this._lastSpecUrlRequested = this.getAttribute("spec-url");
          this.loadSpec(parsed);
        } catch {
          // ignore parse error
        }
      } else if (this.getAttribute("spec-url")) {
        this.loadSpec(this.getAttribute("spec-url"));
      }
    }
  }

  // Cleanup
  disconnectedCallback() {
    if (this._intersectionRaf) {
      cancelAnimationFrame(this._intersectionRaf);
    }
    if (typeof this._observerLockCleanup === "function") {
      this._observerLockCleanup();
      this._observerLockCleanup = null;
    }
    if (this._observerLockTimeout) {
      clearTimeout(this._observerLockTimeout);
      this._observerLockTimeout = null;
    }
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
    }
    if (this._onHashChange) {
      window.removeEventListener("hashchange", this._onHashChange, true);
      this._onHashChange = null;
    }
    this.removeFirstInteractionListeners();
    if (this.mockServer === "true") {
      disableMockServer(this);
    }
    super.disconnectedCallback();
  }

  removeFirstInteractionListeners() {
    if (!this._onFirstInteraction) return;
    INTERACTION_EVENTS.forEach((type) =>
      window.removeEventListener(type, this._onFirstInteraction, { capture: true }),
    );
    this._onFirstInteraction = null;
  }

  /**
   * Behavior of scrolls that move the content to a section. Scrolls before the reader's first input are
   * load-time navigation (location hash, goto-path, back/forward) and jump, like the browser's own
   * fragment navigation. A smooth scroll advances only while the page renders frames, and a background
   * tab renders none: the scroll would not happen, and the active-section tracking would then report the
   * section at the top of the page (and rewrite the URL to it).
   */
  getContentScrollBehavior() {
    return this._readerHasInteracted ? this.scrollBehavior : "instant";
  }

  infoDescriptionHeadingRenderer() {
    const renderer = new marked.Renderer();
    const slugger = new Slugger();
    renderer.heading = ({ text, depth }) =>
      `<h${depth} class="observe-me" id="${slugger.slug(text)}">${text}</h${depth}>`;
    fixRenderedAnchorLinks(renderer);
    return renderer;
  }

  willUpdate(changedProperties) {
    super.willUpdate?.(changedProperties);
    if (this.defaultSchemaTab === "model") {
      this.defaultSchemaTab = "schema";
    }
    if (changedProperties.has("layout") && !this._isDefaultingLayout) {
      this._userSetLayout = Boolean(this.layout && "row, column,".includes(`${this.layout},`));
    }
    if (changedProperties.has("renderStyle") && !this._userSetLayout) {
      const targetLayout =
        this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
      if (this.layout !== targetLayout) {
        this._isDefaultingLayout = true;
        this.layout = targetLayout;
        this.setAttribute("layout", targetLayout);
        this._isDefaultingLayout = false;
      }
    }
  }

  _clearDsdIfNeeded() {
    if (this._hasDsd && this.shadowRoot) {
      this.shadowRoot.replaceChildren();
      if (this.renderOptions) {
        delete this.renderOptions.renderBefore;
      }
      this._hasDsd = false;
    }
  }

  render() {
    if (this._hasDsd && !this.resolvedSpec) {
      return noChange;
    }
    this._clearDsdIfNeeded();

    // adding custom style for RapiDoc: the page's <link> whose href contains the css-file value
    if (this.cssFile) {
      const cssLinkEl = document.querySelector(`link[href*="${CSS.escape(this.cssFile)}"]`);
      if (cssLinkEl) {
        this.shadowRoot.appendChild(cssLinkEl.cloneNode());
      }
    }
    return mainBodyTemplate.call(this);
  }

  get config() {
    return {
      renderStyle: this.renderStyle,
      schemaStyle: this.schemaStyle,
      defaultSchemaTab: this.defaultSchemaTab,
      schemaExpandLevel: this.schemaExpandLevel,
      schemaDescriptionExpanded: this.schemaDescriptionExpanded,
      allowSchemaDescriptionExpandToggle: this.allowSchemaDescriptionExpandToggle,
      schemaHideReadOnly: this.schemaHideReadOnly,
      schemaHideWriteOnly: this.schemaHideWriteOnly,
      fillRequestFieldsWithExample: this.fillRequestFieldsWithExample,
      allowTry: this.allowTry,
      showCurlBeforeTry: this.showCurlBeforeTry,
      fetchCredentials: this.fetchCredentials,
    };
  }

  updated(changedProperties) {
    super.updated?.(changedProperties);
    scheduleHighlight(this.shadowRoot);

    if (changedProperties.has("theme")) {
      if (this.theme === "light" || this.theme === "dark") {
        this.colorScheme = this.theme;
        this.setAttribute("color-scheme", this.colorScheme);
        this.theme = DEFAULT_THEME;
        this.setAttribute("theme", DEFAULT_THEME);
      }
      const normalizedTheme = normalizeTheme(this.theme);
      if (this.theme !== normalizedTheme.theme) {
        this.theme = normalizedTheme.theme;
        this.setAttribute("theme", this.theme);
      }
      applyCustomTheme(this, this.theme);
    }

    if (
      changedProperties.has("primaryColor") &&
      this.primaryColor &&
      isValidHexColor(this.primaryColor)
    ) {
      this.theme = normalizeTheme(this.primaryColor).theme;
      this.setAttribute("theme", this.theme);
      applyCustomTheme(this, this.theme);
    }

    if (changedProperties.has("colorScheme")) {
      if (!this.colorScheme || !"dark, light, system,".includes(`${this.colorScheme},`)) {
        this.colorScheme = "dark";
      }
      if (this.getAttribute("color-scheme") !== this.colorScheme) {
        this.setAttribute("color-scheme", this.colorScheme);
      }
    }

    if (changedProperties.has("mockServer")) {
      if (this.mockServer === "true" && this.resolvedSpec) {
        enableMockServer(this.resolvedSpec, {
          owner: this,
          statusCode: this.mockServerStatusCode,
          statusStrategy: this.mockServerStatusStrategy,
          delay: this.mockServerDelay,
          log: this.mockServerLog,
        });
      } else if (this.mockServer !== "true") {
        disableMockServer(this);
      }
    }
    if (
      changedProperties.has("mockServerStatusCode") ||
      changedProperties.has("mockServerStatusStrategy") ||
      changedProperties.has("mockServerDelay") ||
      changedProperties.has("mockServerLog")
    ) {
      updateMockConfig({
        owner: this,
        statusCode: this.mockServerStatusCode,
        statusStrategy: this.mockServerStatusStrategy,
        delay: this.mockServerDelay,
        log: this.mockServerLog,
      });
    }
    if (changedProperties.has("scrollContainer")) {
      this.initIntersectionObserver();
    }
  }

  observeExpandedContent() {
    // Main Container
    const observeOverviewEls = this.shadowRoot.querySelectorAll(".observe-me");
    observeOverviewEls.forEach((targetEl) => {
      this.intersectionObserver.observe(targetEl);
    });
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (name === "spec-url") {
      if (oldVal !== newVal) {
        // put it at the end of event-loop to load all the attributes
        window.setTimeout(async () => {
          // Already loaded by connectedCallback (or superseded by an inlined spec there)
          if (newVal === this._lastSpecUrlRequested) {
            return;
          }
          await this.loadSpec(newVal);
          // If goto-path is provided and no location-hash is present then try to scroll there
          if (this.gotoPath && !window.location.hash) {
            this.scrollToPath(this.gotoPath);
          }
        }, 0);
      }
    }
    if (
      name === "match-paths" ||
      name === "match-type" ||
      name === "remove-endpoints-with-badge-label-as"
    ) {
      if (oldVal !== newVal) {
        window.setTimeout(async () => {
          await this.loadSpec(this.specUrl);
        }, 0);
      }
    }
    if (name === "scroll-offset" && oldVal !== newVal) {
      window.setTimeout(() => this.initIntersectionObserver(), 0);
    }
    if (name === "layout") {
      if (!this._isDefaultingLayout) {
        if (newVal !== null && "row, column,".includes(`${newVal},`)) {
          this._userSetLayout = true;
          this.layout = newVal;
        } else if (newVal === null) {
          this._userSetLayout = false;
          const defaultLayout =
            this.renderStyle === "read" || this.renderStyle === "focused" ? "column" : "row";
          this._isDefaultingLayout = true;
          this.layout = defaultLayout;
          this.setAttribute("layout", defaultLayout);
          this._isDefaultingLayout = false;
        }
      }
    }
    if (name === "render-style") {
      if (newVal === "read") {
        window.setTimeout(() => {
          this.observeExpandedContent();
        }, 100);
      } else {
        this.intersectionObserver.disconnect();
      }
      if (!this._userSetLayout) {
        const defaultLayout = newVal === "read" || newVal === "focused" ? "column" : "row";
        this._isDefaultingLayout = true;
        this.layout = defaultLayout;
        this.setAttribute("layout", defaultLayout);
        this._isDefaultingLayout = false;
      }
    }
    if (name === "api-key-name" || name === "api-key-location" || name === "api-key-value") {
      let updateSelectedApiKey = false;
      let apiKeyName = "";
      let apiKeyLocation = "";
      let apiKeyValue = "";

      if (name === "api-key-name") {
        if (this.getAttribute("api-key-location") && this.getAttribute("api-key-value")) {
          apiKeyName = newVal;
          apiKeyLocation = this.getAttribute("api-key-location");
          apiKeyValue = this.getAttribute("api-key-value");
          updateSelectedApiKey = true;
        }
      } else if (name === "api-key-location") {
        if (this.getAttribute("api-key-name") && this.getAttribute("api-key-value")) {
          apiKeyLocation = newVal;
          apiKeyName = this.getAttribute("api-key-name");
          apiKeyValue = this.getAttribute("api-key-value");
          updateSelectedApiKey = true;
        }
      } else if (name === "api-key-value") {
        if (this.getAttribute("api-key-name") && this.getAttribute("api-key-location")) {
          apiKeyValue = newVal;
          apiKeyLocation = this.getAttribute("api-key-location");
          apiKeyName = this.getAttribute("api-key-name");
          updateSelectedApiKey = true;
        }
      }

      if (updateSelectedApiKey) {
        // While the spec is loading, resolvedSpec is a placeholder without securitySchemes
        if (this.resolvedSpec?.securitySchemes) {
          const rapiDocApiKey = this.resolvedSpec.securitySchemes.find(
            (v) => v.securitySchemeId === rapidocApiKey,
          );
          if (!rapiDocApiKey) {
            this.resolvedSpec.securitySchemes.push({
              securitySchemeId: rapidocApiKey,
              description: "api-key provided in rapidoc element attributes",
              type: "apiKey",
              name: apiKeyName,
              in: apiKeyLocation,
              value: apiKeyValue,
              finalKeyValue: apiKeyValue,
            });
          } else {
            rapiDocApiKey.name = apiKeyName;
            rapiDocApiKey.in = apiKeyLocation;
            rapiDocApiKey.value = apiKeyValue;
            rapiDocApiKey.finalKeyValue = apiKeyValue;
          }
          this.requestUpdate();
        }
      }
    }
    super.attributeChangedCallback(name, oldVal, newVal);
  }

  onSpecUrlChange() {
    this.setAttribute("spec-url", this.shadowRoot.getElementById("spec-url").value);
  }

  onSpecFileChange(e) {
    this.setAttribute("spec-file", this.shadowRoot.getElementById("spec-file").value);
    const specFile = e.target.files[0];
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const specObj = JSON.parse(reader.result);
        this.loadSpec(specObj);
        this.shadowRoot.getElementById("spec-url").value = "";
      } catch {
        console.error("RapiDoc: Unable to read or parse json");
      }
    };
    // Read the Text file
    reader.readAsText(specFile);
  }

  onFileLoadClick() {
    this.shadowRoot.getElementById("spec-file").click();
  }

  onSearchChange(e) {
    // The event may come from the ↩ icon (a div without `value`): read the nav-bar input instead
    const input =
      e.target instanceof HTMLInputElement
        ? e.target
        : this.shadowRoot.getElementById("nav-bar-search");
    this.searchVal = input?.value ?? "";
    this.resolvedSpec.tags.forEach((tag) =>
      tag.paths.filter((path) => {
        if (this.searchVal) {
          if (getMatchedPaths(this.searchVal, path, tag.name)) {
            tag.expanded = true;
          }
        }
      }),
    );
    this.resolvedSpec.components.forEach((component) =>
      component.subComponents.filter((v) => {
        v.expanded = false;
        if (getMatchedComponents(this.searchVal, v)) {
          v.expanded = true;
        }
      }),
    );
    this.requestUpdate();
  }

  onClearSearch() {
    const searchEl = this.shadowRoot.getElementById("nav-bar-search");
    searchEl.value = "";
    this.searchVal = "";
    this.resolvedSpec.components.forEach((component) =>
      component.subComponents.filter((v) => {
        v.expanded = true;
      }),
    );
  }

  onOpenNavBarToggle() {
    const navBarEL = this.shadowRoot.getElementById("nav-bar");
    navBarEL.classList.toggle("floating-nav");
  }

  onShowAdvancedSearchClicked() {
    this.shadowRoot.getElementById("advanced-search-dialog").showModal();
  }
  onAdvancedSearchClose() {
    this.shadowRoot.getElementById("advanced-search-dialog").close();
  }

  // Public Method
  async loadSpec(specUrl) {
    if (!specUrl) {
      return;
    }
    if (typeof specUrl === "string") {
      this._lastSpecUrlRequested = specUrl;
    }
    // Each load gets a token; a load whose token is no longer current never applies its result
    const token = ++this._specLoadSeq;
    this.searchVal = "";
    try {
      if (!this._hasDsd) {
        this.resolvedSpec = {
          specLoadError: false,
          isSpecLoading: true,
          tags: [],
        };
        this.loading = true;
      }
      this.loadFailed = false;
      const spec = await ProcessSpec.call(
        this,
        specUrl,
        this.generateMissingTags === "true",
        this.sortTags === "true",
        this.sortSchemas === "true",
        this.getAttribute("sort-endpoints-by"),
        this.getAttribute("api-key-name"),
        this.getAttribute("api-key-location"),
        this.getAttribute("api-key-value"),
        this.getAttribute("server-url"),
        this.matchPaths,
        this.matchType,
        this.removeEndpointsWithBadgeLabelAs,
        this.showUnusedTags === "true",
      );
      if (token !== this._specLoadSeq) {
        return; // a newer load started while this one was parsing
      }
      this.loading = false;
      this.afterSpecParsedAndValidated(spec, token);
    } catch (err) {
      if (token !== this._specLoadSeq) {
        return;
      }
      this.loading = false;
      this._clearDsdIfNeeded();
      this.loadFailed = true;
      this.resolvedSpec = null;
      console.error(`RapiDoc: Unable to resolve the API spec..  ${err.message}`);
    }
  }

  async afterSpecParsedAndValidated(spec, token = this._specLoadSeq) {
    this._clearDsdIfNeeded();
    this.resolvedSpec = spec;
    this.selectedServer = undefined;
    if (this.defaultApiServerUrl) {
      if (this.defaultApiServerUrl === this.serverUrl) {
        this.selectedServer = {
          url: this.serverUrl,
          computedUrl: this.serverUrl,
        };
      } else if (this.resolvedSpec.servers) {
        this.selectedServer = this.resolvedSpec.servers.find(
          (v) => v.url === this.defaultApiServerUrl,
        );
      }
    }
    if (!this.selectedServer) {
      if (this.resolvedSpec.servers) {
        this.selectedServer = this.resolvedSpec.servers[0];
      }
    }
    this.requestUpdate();
    while (!(await this.updateComplete));
    if (token !== this._specLoadSeq) {
      return; // a newer load took over while this one was rendering; it fires its own spec-loaded
    }
    const specLoadedEvent = new CustomEvent("spec-loaded", { detail: spec });
    this.dispatchEvent(specLoadedEvent);

    if (this.mockServer === "true") {
      enableMockServer(this.resolvedSpec, {
        owner: this,
        statusCode: this.mockServerStatusCode,
        statusStrategy: this.mockServerStatusStrategy,
        delay: this.mockServerDelay,
        log: this.mockServerLog,
      });
    }

    // Initiate IntersectionObserver and put it at the end of event loop, to allow loading all the child elements (must for larger specs)
    this.intersectionObserver.disconnect();
    if (this._intersectingElements) {
      this._intersectingElements.clear();
    }
    if (this.renderStyle === "read") {
      await sleep(100);
      if (token !== this._specLoadSeq) {
        return;
      }
      this.observeExpandedContent(); // This will auto-highlight the selected nav-item in read-mode
    }

    this.isIntersectionObserverActive = true;

    // On first time Spec load, try to navigate to location hash if provided
    const elementId = this.getElementIDFromURL();

    if (elementId) {
      if (this.renderStyle === "view") {
        this.expandAndGotoOperation(elementId, true, true);
      } else {
        this.scrollToPath(elementId);
      }
    } else if (this.gotoPath) {
      this.scrollToPath(this.gotoPath);
    } else if (this.renderStyle === "focused") {
      // If goto-path is provided and no location-hash is present then try to scroll to default element
      if (!this.gotoPath) {
        const defaultElementId =
          this.showInfo === "true"
            ? "overview"
            : this.resolvedSpec.tags.find((t) => t.paths.length > 0)?.paths[0]?.elementId;
        if (defaultElementId) {
          this.scrollToPath(defaultElementId);
        }
      }
    }
  }

  /**
   * Return the URL from where is served the RapiDoc component, removing any hash
   */
  getComponentBaseURL() {
    return window.location.href.split("#")[0];
  }

  /**
   * From the URL return the ID of the element from the location hash
   */
  getElementIDFromURL() {
    return window.location.hash ? window.location.hash.substring(1) : "";
  }

  replaceHistoryState(hashId) {
    const baseURL = this.getComponentBaseURL();
    window.history.replaceState(null, null, hashId ? `${baseURL}#${hashId}` : baseURL);
  }

  expandAndGotoOperation(elementId, scrollToElement = true) {
    if (!this.resolvedSpec) {
      return;
    }
    // Expand full operation and tag
    let isExpandingNeeded = true;
    const tmpElementId = elementId.indexOf("#") === -1 ? elementId : elementId.substring(1);
    if (
      tmpElementId.startsWith("overview") ||
      tmpElementId === "servers" ||
      tmpElementId === "auth"
    ) {
      isExpandingNeeded = false;
    } else {
      for (let i = 0; i < this.resolvedSpec.tags?.length; i++) {
        const tag = this.resolvedSpec.tags[i];
        const path = tag.paths?.find((p) => p.elementId === elementId);
        if (path) {
          if (path.expanded && tag.expanded) {
            isExpandingNeeded = false;
          } else {
            path.expanded = true;
            tag.expanded = true;
          }
        }
      }
      if (this.resolvedSpec.tagGroups) {
        for (let g = 0; g < this.resolvedSpec.tagGroups.length; g++) {
          const group = this.resolvedSpec.tagGroups[g];
          const hasPath = group.tags?.some((t) => t.paths?.some((p) => p.elementId === elementId));
          if (hasPath && !group.expanded) {
            group.expanded = true;
            isExpandingNeeded = true;
          }
        }
      }
    }
    if (scrollToElement) {
      // requestUpdate() and delay required, else we cant find element
      if (isExpandingNeeded) {
        this.requestUpdate();
      }
      window.setTimeout(
        () => {
          const firstNavEl = this.shadowRoot.querySelector(".first-nav-item");
          const isFirst = firstNavEl && firstNavEl.dataset.contentId === tmpElementId;
          const gotoEl = this.shadowRoot.getElementById(tmpElementId);
          if (gotoEl) {
            const mainContentEl = this.shadowRoot.querySelector(".main-content");
            let targetTop: number;
            if (this.autoHeight === "true") {
              this.scrollAutoHeightTarget(gotoEl, tmpElementId);
            } else if (isFirst) {
              targetTop = 0;
              this.lockObserverDuringScroll(gotoEl, tmpElementId, targetTop);
              if (mainContentEl) {
                mainContentEl.scrollTo({ top: 0, behavior: this.getContentScrollBehavior() });
              } else {
                gotoEl.scrollIntoView({
                  behavior: this.getContentScrollBehavior(),
                  block: "start",
                });
              }
            } else if (mainContentEl) {
              const containerRect = mainContentEl.getBoundingClientRect();
              const targetRect = gotoEl.getBoundingClientRect();
              targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
              this.lockObserverDuringScroll(gotoEl, tmpElementId, targetTop);
              mainContentEl.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
            } else {
              this.lockObserverDuringScroll(gotoEl, tmpElementId);
              gotoEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
            }
            this._activeElementId = tmpElementId;
            this.dispatchEvent(
              new CustomEvent("nav-active-change", {
                detail: {
                  id: tmpElementId,
                  elementId: tmpElementId,
                },
                bubbles: true,
                composed: true,
              }),
            );
            if (this.updateRoute === "true") {
              this.replaceHistoryState(tmpElementId);
            }
          }
        },
        isExpandingNeeded ? 150 : 0,
      );
    }
  }

  isValidTopId(id) {
    return id.startsWith("overview") || id === "servers" || id === "auth";
  }

  isValidPathId(id) {
    if (id === "overview" && this.showInfo) {
      return true;
    }
    if (id === "servers" && this.allowServerSelection) {
      return true;
    }
    if (id === "auth" && this.allowAuthentication) {
      return true;
    }
    if (id.startsWith("tag--")) {
      return this.resolvedSpec?.tags?.find((tag) => tag.elementId === id);
    }
    return this.resolvedSpec?.tags?.find((tag) => tag.paths.find((path) => path.elementId === id));
  }

  initIntersectionObserver() {
    if (this.intersectionObserver) {
      this.intersectionObserver.disconnect();
    }
    let container = null;
    if (this.scrollContainer && typeof document !== "undefined") {
      try {
        container = document.querySelector(this.scrollContainer);
        const isAncestor = Boolean(
          container &&
          (container.contains(this) ||
            (this.assignedSlot && container.contains(this.assignedSlot)) ||
            (container.shadowRoot && container.shadowRoot.contains(this.assignedSlot))),
        );
        if (container && !isAncestor) {
          // If the specified element is not an ancestor of <rapi-doc>, IntersectionObserver cannot observe inside it
          console.warn(
            `RapiDoc: The element specified by scroll-container ("${this.scrollContainer}") is not an ancestor of <rapi-doc>. Falling back to auto-detection.`,
          );
          container = null;
        }
      } catch {
        // Ignore invalid query selector
      }
    }
    if (!container && typeof window !== "undefined") {
      let parent: any = this.assignedSlot ? this.assignedSlot.parentElement : this.parentElement;
      while (parent && parent !== document.body && parent !== document.documentElement) {
        const overflowY = window.getComputedStyle(parent).overflowY;
        if (overflowY === "auto" || overflowY === "scroll") {
          container = parent;
          break;
        }
        parent =
          parent.parentElement ||
          (parent.getRootNode && parent.getRootNode() instanceof ShadowRoot
            ? (parent.getRootNode() as ShadowRoot).host
            : null);
      }
    }
    this._scrollRootContainer = container;
    const intersectionObserverOptions = {
      root: container,
      // Headings that slide under a sticky page header (getScrollOffset) count as scrolled past.
      rootMargin: `-${this.getScrollOffset()}px 0px -50% 0px`,
      threshold: 0,
    };
    this.intersectionObserver = new IntersectionObserver((entries) => {
      this.onIntersect(entries);
    }, intersectionObserverOptions);
    if (this.isIntersectionObserverActive) {
      this.observeExpandedContent();
    }
  }

  onIntersect(entries) {
    if (this.isIntersectionObserverActive === false) {
      return;
    }

    if (!this._intersectingElements) {
      this._intersectingElements = new Map();
    }

    for (const entry of entries) {
      if (entry.isIntersecting && entry.intersectionRatio > 0) {
        this._intersectingElements.set(entry.target.id, entry.target);
      } else {
        this._intersectingElements.delete(entry.target.id);
      }
    }

    if (this._intersectingElements.size === 0) {
      return;
    }

    const activeCandidates = Array.from(this._intersectingElements.values()).filter(
      (el) => el.isConnected,
    );
    if (activeCandidates.length === 0) {
      return;
    }

    const containerTop =
      (this._scrollRootContainer ? this._scrollRootContainer.getBoundingClientRect().top : 0) +
      this.getScrollOffset();
    const visibleHeadings = [];
    const scrolledPast = [];

    for (const el of activeCandidates) {
      const top = el.getBoundingClientRect().top - containerTop;
      // A section scrolled to (scrollToPath) can end up a fraction of a pixel above the top: the
      // browser rounds the scroll position to device pixels. It still counts as visible.
      if (top > -1) {
        visibleHeadings.push({ el, top });
      } else {
        scrolledPast.push({ el, top });
      }
    }

    let bestEl;
    if (visibleHeadings.length > 0) {
      // Preference to the topmost heading visible in the viewport/reading zone
      visibleHeadings.sort((a, b) => a.top - b.top);
      bestEl = visibleHeadings[0].el;
    } else {
      // If all visible candidates have their headings scrolled past top (< 0),
      // pick the one whose top is closest to 0 (the section currently filling the reading area)
      scrolledPast.sort((a, b) => b.top - a.top);
      bestEl = scrolledPast[0].el;
    }

    const targetId = bestEl.id;
    if (this._activeElementId === targetId) {
      return;
    }
    this._activeElementId = targetId;

    if (this._intersectionRaf) {
      cancelAnimationFrame(this._intersectionRaf);
    }

    this._intersectionRaf = requestAnimationFrame(() => {
      // 1. Dispatch custom event so outer portals/host apps can sync active state
      let resolvedOperationId = targetId;
      if (targetId.startsWith("tag--") && this.resolvedSpec?.tags) {
        const matchingTag = this.resolvedSpec.tags.find((t) => t.elementId === targetId);
        if (matchingTag?.paths?.[0]?.elementId) {
          resolvedOperationId = matchingTag.paths[0].elementId;
        }
      }

      this.dispatchEvent(
        new CustomEvent("nav-active-change", {
          detail: {
            id: targetId,
            elementId: targetId,
            operationId: resolvedOperationId,
          },
          bubbles: true,
          composed: true,
        }),
      );

      if (this.updateRoute === "true") {
        this.replaceHistoryState(targetId);
      }

      // 2. If internal navbar is active (show-side-nav="true"), update Shadow DOM elements
      const newNavEl = this.shadowRoot.getElementById(`link-${targetId}`);
      if (newNavEl) {
        const oldNavEl = this.shadowRoot.querySelector(
          ".nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active",
        );

        if (oldNavEl && oldNavEl !== newNavEl) {
          oldNavEl.classList.remove("active");
          oldNavEl.part.remove("section-navbar-active-item");
        }

        expandParentNavContainers(newNavEl);
        newNavEl.classList.add("active");
        newNavEl.part.add("section-navbar-active-item");

        // Scroll sidebar only if the active item is out of view
        const navScrollEl = newNavEl.closest(".nav-scroll");
        if (navScrollEl) {
          const headerEl =
            navScrollEl.querySelector(".nav-bar-header") ||
            navScrollEl.querySelector(".nav-bar-search-container");
          const topOffset = headerEl ? headerEl.offsetHeight : 0;

          if (newNavEl.offsetTop <= topOffset + 30) {
            if (navScrollEl.scrollTop > 0) {
              const behavior = this.scrollBehavior === "auto" ? "auto" : "smooth";
              navScrollEl.scrollTo({ top: 0, behavior });
            }
          } else {
            const scrollRect = navScrollEl.getBoundingClientRect();
            const itemRect = newNavEl.getBoundingClientRect();
            const isOutOfView =
              itemRect.top < scrollRect.top + topOffset + 15 ||
              itemRect.bottom > scrollRect.bottom - 40;
            if (isOutOfView) {
              const behavior = this.scrollBehavior === "auto" ? "auto" : "smooth";
              // Land the item below the sticky search header (which sits inside the scroll area), not under it
              const targetTop =
                itemRect.top - scrollRect.top + navScrollEl.scrollTop - topOffset - 20;
              navScrollEl.scrollTo({ top: targetTop, behavior });
            }
          }
        }
      }
    });
  }

  // Called by anchor tags created using markdown
  handleHref(e) {
    if (e.target.tagName.toLowerCase() === "a") {
      if (e.target.getAttribute("href").startsWith("#")) {
        const gotoEl = this.shadowRoot.getElementById(
          e.target.getAttribute("href").replace("#", ""),
        );
        if (gotoEl) {
          const mainContentEl = this.shadowRoot.querySelector(".main-content");
          if (this.autoHeight === "true" || this.hasAttribute("auto-height")) {
            this.scrollAutoHeightTarget(gotoEl, gotoEl.id);
          } else if (mainContentEl) {
            const containerRect = mainContentEl.getBoundingClientRect();
            const targetRect = gotoEl.getBoundingClientRect();
            const targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
            mainContentEl.scrollTo({ top: targetTop, behavior: this.scrollBehavior });
          } else {
            gotoEl.scrollIntoView({ behavior: this.scrollBehavior, block: "start" });
          }
        }
      }
    }
  }

  /**
   * Height of the area at the top of the scroll root that sticky or fixed page headers cover, in px.
   * Only applies with auto-height, where the page (or `scroll-container`) scrolls instead of RapiDoc.
   * Uses the `scroll-offset` attribute when set, otherwise the scroll root's `scroll-padding-top`,
   * so pages that already reserve space for their header need no extra configuration.
   */
  getScrollOffset(): number {
    if (
      !(this.autoHeight === "true" || this.hasAttribute("auto-height")) ||
      typeof window === "undefined"
    ) {
      return 0;
    }
    const explicit = parseFloat(this.scrollOffset);
    if (Number.isFinite(explicit)) {
      return explicit;
    }
    const root: HTMLElement =
      this._scrollRootContainer ||
      (document.scrollingElement as HTMLElement) ||
      document.documentElement;
    const padding = getComputedStyle(root).scrollPaddingTop;
    const value = parseFloat(padding);
    if (!Number.isFinite(value)) {
      return 0;
    }
    return padding.endsWith("%") ? (value / 100) * root.clientHeight : value;
  }

  /**
   * auto-height: scrolls the page, or the `scroll-container`, so that `targetEl` starts just below
   * the area covered by sticky headers (see getScrollOffset).
   */
  scrollAutoHeightTarget(targetEl: HTMLElement, targetId: string) {
    const top = targetEl.getBoundingClientRect().top - this.getScrollOffset();
    const container = this._scrollRootContainer;
    if (container) {
      const targetTop = top - container.getBoundingClientRect().top + container.scrollTop;
      this.lockObserverDuringScroll(targetEl, targetId, targetTop);
      container.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
    } else {
      const targetTop = top + window.scrollY;
      this.lockObserverDuringScroll(targetEl, targetId, targetTop);
      window.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
    }
  }

  /**
   * Temporarily deactivates the IntersectionObserver during navigation/programmatic scrolling,
   * preventing intermediate elements from hijacking the active navbar item while smooth scrolling.
   * Re-activates the observer once scrolling finishes (via 'scrollend', debounced scroll fallback, or safety timeout).
   *
   * @param {HTMLElement | null} targetEl - The element being scrolled to
   * @param {string} targetId - Element / operation identifier
   * @param {number} [targetTop] - Target scroll offset if known
   */
  lockObserverDuringScroll(
    targetEl: HTMLElement | null,
    targetId: string = "",
    targetTop?: number,
  ) {
    if (this._intersectionRaf) {
      cancelAnimationFrame(this._intersectionRaf);
      this._intersectionRaf = null;
    }

    if (typeof this._observerLockCleanup === "function") {
      this._observerLockCleanup();
      this._observerLockCleanup = null;
    }
    if (this._observerLockTimeout) {
      clearTimeout(this._observerLockTimeout);
      this._observerLockTimeout = null;
    }

    // onIntersect tracks elements by their DOM id; targetId can be a shorter slug of the same element
    // (e.g. "get-route-estimate" for "route-planning--get-route-estimate"). Keying by the slug would leave
    // an entry the observer never removes, pinning the active item to this element.
    const observedId = targetEl?.id || targetId;
    this.isIntersectionObserverActive = false;
    if (this._intersectingElements) {
      this._intersectingElements.clear();
      if (targetEl && observedId) {
        this._intersectingElements.set(observedId, targetEl);
      }
    }
    if (observedId) {
      this._activeElementId = observedId;
    }

    const isAutoHeight = this.autoHeight === "true" || this.hasAttribute("auto-height");
    const scrollContainer: any = isAutoHeight
      ? this._scrollRootContainer || (typeof window !== "undefined" ? window : null)
      : this.shadowRoot?.querySelector(".main-content") ||
        (typeof window !== "undefined" ? window : null);

    if (!scrollContainer || typeof window === "undefined") {
      this.isIntersectionObserverActive = true;
      return;
    }

    let isUnlocked = false;
    const unlock = () => {
      if (isUnlocked) return;
      isUnlocked = true;
      if (cleanup) {
        cleanup();
        cleanup = null;
        this._observerLockCleanup = null;
      }
      if (this._observerLockTimeout) {
        clearTimeout(this._observerLockTimeout);
        this._observerLockTimeout = null;
      }

      if (targetEl && observedId && this._intersectingElements) {
        this._intersectingElements.set(observedId, targetEl);
      }
      this.isIntersectionObserverActive = true;
    };

    let debounceTimer: any = null;
    const onScroll = () => {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(unlock, 120);
    };

    const onScrollEnd = () => {
      unlock();
    };

    let cleanup: any = () => {
      scrollContainer.removeEventListener?.("scrollend", onScrollEnd);
      window.removeEventListener?.("scrollend", onScrollEnd);
      scrollContainer.removeEventListener?.("scroll", onScroll);
      window.removeEventListener?.("scroll", onScroll);
      clearTimeout(debounceTimer);
    };
    this._observerLockCleanup = cleanup;

    if ("onscrollend" in window) {
      scrollContainer.addEventListener?.("scrollend", onScrollEnd, { once: true, passive: true });
      window.addEventListener?.("scrollend", onScrollEnd, { once: true, passive: true });
    }
    scrollContainer.addEventListener?.("scroll", onScroll, { passive: true });
    window.addEventListener?.("scroll", onScroll, { passive: true });

    const currentScrollTop =
      scrollContainer === window ? window.scrollY : scrollContainer.scrollTop || 0;
    const isZeroDistance =
      typeof targetTop === "number" && Math.abs(targetTop - currentScrollTop) < 3;
    const maxSafetyDelay = isZeroDistance
      ? 60
      : this.getContentScrollBehavior() === "smooth"
        ? 1200
        : 100;
    this._observerLockTimeout = setTimeout(unlock, maxSafetyDelay);
  }

  /**
   * Called by
   *  - onClick of Navigation Bar
   *  - onClick of Advanced Search items
   *
   * Functionality:
   *  1. First deactivate IntersectionObserver
   *  2. Scroll to the element
   *  3. Activate IntersectionObserver (after scroll completes)
   */
  async scrollToEventTarget(event, scrollNavItemToView = true) {
    if (!(event.type === "click" || (event.type === "keyup" && event.keyCode === 13))) {
      return;
    }
    const navEl = ((event.target as HTMLElement)?.closest?.("[data-content-id]") ||
      event.target) as HTMLElement;
    if (!navEl?.dataset?.contentId) {
      return;
    }
    if (this.renderStyle === "focused") {
      const requestEl = this.shadowRoot.querySelector("api-request");
      if (typeof requestEl?.beforeNavigationFocusedMode === "function") {
        requestEl.beforeNavigationFocusedMode();
      }
    }
    const isFirstNavItem = navEl.dataset.firstNavItem === "true";
    this.scrollToPath(navEl.dataset.contentId, true, scrollNavItemToView, isFirstNavItem);
  }

  // Public Method - Returns the DOM element in ShadowRoot matching elementId, operationId, or slug
  getTargetElement(elementId: string): HTMLElement | null {
    if (!elementId || !this.shadowRoot) return null;
    return (this.shadowRoot.getElementById(elementId) ||
      this.shadowRoot.querySelector(`[data-op-slug="${CSS.escape(elementId)}"]`) ||
      this.shadowRoot.querySelector(`[id$="--${CSS.escape(elementId)}"]`) ||
      this.shadowRoot.getElementById(
        elementId.replace(/^[a-z0-9_-]+--/, ""),
      )) as HTMLElement | null;
  }

  /**
   * Every operation can also be reached by its method + path anchor (methodPathSlug, e.g.
   * "get-routes-id-estimate"), which stays the same when its operationId or summary changes. The
   * comparison ignores case and punctuation, so older RapiDoc anchors ("get-/routes/-id-/estimate") work too.
   * Returns the operation's own element id for such an anchor, otherwise elementId unchanged.
   */
  resolveOperationAlias(elementId: string): string {
    if (!elementId || this.getTargetElement(elementId)) return elementId;
    const normalize = (id: string) =>
      id
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, "-")
        .replace(/^-+|-+$/g, "");
    const wanted = normalize(elementId);
    for (const tag of this.resolvedSpec?.tags || []) {
      const op = tag.paths?.find((p) => normalize(methodPathSlug(p.method, p.path)) === wanted);
      if (op) return op.elementId;
    }
    return elementId;
  }

  // Public Method - Binds an external navbar to this <rapi-doc> element
  bindNavBar(sidebar: HTMLElement | string, options?: any) {
    return bindNavBar(sidebar, this, options);
  }

  // Public Method (scrolls to a given path and highlights the left-nav selection)
  async scrollTo(elementId) {
    return this.scrollToPath(elementId);
  }

  // Public Method (scrolls to a given path and highlights the left-nav selection)
  async scrollToPath(
    elementId,
    expandPath = true,
    scrollNavItemToView = true,
    isFirstNavItem = false,
  ) {
    elementId = this.resolveOperationAlias(elementId);
    if (this.renderStyle === "focused") {
      // for focused mode update this.focusedElementId to update the rendering, else it wont find the needed html elements
      // focusedElementId will get validated in the template
      this.focusedElementId = elementId;
      await sleep(0);
    }
    if (this.renderStyle === "view") {
      this.expandAndGotoOperation(elementId, expandPath, true);
    } else {
      const contentEl = this.getTargetElement(elementId);
      const isValidElementId = Boolean(contentEl);
      if (contentEl) {
        const firstNavEl = this.shadowRoot.querySelector(".first-nav-item");
        const isFirst =
          isFirstNavItem || (firstNavEl && firstNavEl.dataset.contentId === elementId);
        const mainContentEl = this.shadowRoot.querySelector(".main-content");
        let targetTop: number;

        if (this.autoHeight === "true" || this.hasAttribute("auto-height")) {
          this.scrollAutoHeightTarget(contentEl, elementId);
        } else if (isFirst) {
          targetTop = 0;
          this.lockObserverDuringScroll(contentEl, elementId, targetTop);
          if (mainContentEl) {
            mainContentEl.scrollTo({ top: 0, behavior: this.getContentScrollBehavior() });
          } else {
            contentEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
          }
        } else if (mainContentEl) {
          const containerRect = mainContentEl.getBoundingClientRect();
          const targetRect = contentEl.getBoundingClientRect();
          targetTop = targetRect.top - containerRect.top + mainContentEl.scrollTop;
          this.lockObserverDuringScroll(contentEl, elementId, targetTop);
          mainContentEl.scrollTo({ top: targetTop, behavior: this.getContentScrollBehavior() });
        } else {
          this.lockObserverDuringScroll(contentEl, elementId);
          contentEl.scrollIntoView({ behavior: this.getContentScrollBehavior(), block: "start" });
        }
      }
      if (isValidElementId) {
        // In focused mode, <api-request> and <api-response> component instances are reused across endpoint switches
        // to avoid expensive DOM re-creation. Reset their selection state so selections from the previous operation
        // (such as alternative request bodies or non-default status codes) do not persist into the new operation.
        if (this.renderStyle === "focused") {
          const requestEl = this.shadowRoot.querySelector("api-request");
          if (typeof requestEl?.afterNavigationFocusedMode === "function") {
            requestEl.afterNavigationFocusedMode();
          }
          const responseEl = this.shadowRoot.querySelector("api-response");
          if (typeof responseEl?.resetSelection === "function") {
            responseEl.resetSelection();
          }
        }

        // Update Location Hash
        this._activeElementId = contentEl?.id || elementId;
        this.dispatchEvent(
          new CustomEvent("nav-active-change", {
            detail: {
              id: elementId,
              elementId,
            },
            bubbles: true,
            composed: true,
          }),
        );
        if (this.updateRoute === "true") {
          this.replaceHistoryState(elementId);
        }

        // Update NavBar View and Styles
        const newNavEl = this.shadowRoot.getElementById(`link-${elementId}`);

        if (newNavEl) {
          expandParentNavContainers(newNavEl);
          if (scrollNavItemToView) {
            const navScrollEl = this.shadowRoot.querySelector(".nav-scroll");
            if (navScrollEl) {
              const scrollRect = navScrollEl.getBoundingClientRect();
              const itemRect = newNavEl.getBoundingClientRect();
              const targetTop =
                itemRect.top -
                scrollRect.top +
                navScrollEl.scrollTop -
                navScrollEl.clientHeight / 2;
              navScrollEl.scrollTo({ top: targetTop, behavior: this.scrollBehavior });
            } else {
              newNavEl.scrollIntoView({ behavior: this.scrollBehavior, block: "center" });
            }
          }
          await sleep(0);
          const oldNavEl = this.shadowRoot.querySelector(
            ".nav-bar-tag.active, .nav-bar-path.active, .nav-bar-info.active, .nav-bar-h1.active, .nav-bar-h2.active, .operations.active",
          );
          if (oldNavEl) {
            oldNavEl.classList.remove("active");
            oldNavEl.part.remove("active");
            oldNavEl.part.remove("section-navbar-active-item");
          }
          newNavEl.classList.add("active"); // must add the class after scrolling
          newNavEl.part.add("section-navbar-active-item");
          // this.requestUpdate();
        }
      }
    }
  }

  // Public Method - to update security-scheme of type http
  setHttpUserNameAndPassword(securitySchemeId, username, password) {
    return applyApiKey.call(this, securitySchemeId, username, password);
  }

  // Public Method - to update security-scheme of type apiKey or OAuth.
  // options.note: where the key came from (e.g. "Using your sign-in"), shown in place of the scheme's form
  setApiKey(securitySchemeId, apiKeyValue, options: { note?: string } = {}) {
    return applyApiKey.call(this, securitySchemeId, "", "", apiKeyValue, options?.note || "");
  }

  // Public Method
  removeAllSecurityKeys() {
    return onClearAllApiKeys.call(this);
  }

  // Public Method
  setApiServer(apiServerUrl) {
    // return apiServerUrl;
    return setApiServer.call(this, apiServerUrl);
  }

  // True when the `mcp` attribute asks for the Export MCP Server button
  get mcpEnabled() {
    return parseMcpEnabled(this.mcp);
  }

  /**
   * The <rapi-flow> on the page whose Arazzo document the export takes workflow tools from: the
   * one whose `for` names this element, else one without `for`. Nothing when rapi-flow is not
   * loaded.
   */
  private findArazzoDocument() {
    if (typeof customElements === "undefined" || !customElements.get("rapi-flow")) return undefined;
    const flows = [...document.querySelectorAll("rapi-flow")] as any[];
    return (
      flows.find((f) => this.id && f.getAttribute("for") === this.id && f.arazzoDocument) ||
      flows.find((f) => !f.getAttribute("for") && f.arazzoDocument)
    );
  }

  private mcpOptions(options) {
    const flow = this.findArazzoDocument();
    return {
      serverName: this.mcpServerName,
      arazzo: flow?.arazzoDocument,
      // <rapi-flow> carries the workflow runtime; this bundle does not
      workflowRuntime: flow?.workflowRuntimeSource,
      ...options,
    };
  }

  // Public Method - Builds the MCP manifest and the files of the exported project in memory
  generateMcpServer(options = {}) {
    const spec = this.resolvedSpec || this.openApiSpec;
    if (!spec) return null;
    return mcpServerGenerator(spec, this.mcpOptions(options));
  }

  // Public Method - Downloads the exported MCP project as a zip
  downloadMcpServer(options = {}) {
    const result = this.generateMcpServer(options);
    if (!result) return;
    const blob = new Blob([createZip(result.files)], { type: "application/zip" });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.download = `${result.manifest.server.name.replace(/_/g, "-")}-mcp.zip`;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(link.href);
  }

  // Public Method - Returns token budget diagnostics for MCP tooling
  getMcpBudget(options = {}) {
    const spec = this.resolvedSpec || this.openApiSpec;
    if (!spec) return null;
    return estimateTokenCost(buildMcpManifest(spec, this.mcpOptions(options)));
  }

  // Event handler for Advanced Search text-inputs and checkboxes
  onAdvancedSearch(ev, delay) {
    const eventTargetEl = ev.target;
    clearTimeout(this.timeoutId);
    this.timeoutId = setTimeout(() => {
      let searchInputEl;
      if (eventTargetEl.type === "text") {
        searchInputEl = eventTargetEl;
      } else {
        searchInputEl = eventTargetEl
          .closest(".advanced-search-options")
          .querySelector("input[type=text]");
      }
      const searchOptions = [
        ...eventTargetEl.closest(".advanced-search-options").querySelectorAll("input:checked"),
      ].map((v) => v.id);
      this.advancedSearchMatches = advancedSearch(
        searchInputEl.value,
        this.resolvedSpec.tags,
        searchOptions,
      );
    }, delay);
  }
}
