import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import { scheduleHighlight } from "~/utils/highlighter";
import ParamListStyles from "~/styles/param-list-styles";
import FlexStyles from "~/styles/flex-styles";
import InputStyles from "~/styles/input-styles";
import FontStyles from "~/styles/font-styles";
import BorderStyles from "~/styles/border-styles";
import TabStyles from "~/styles/tab-styles";
import MicrolighterStyles from "~/styles/microlighter-styles";
import CustomStyles from "~/styles/custom-styles";
import { constructFetchURL } from "~/utils/url-utils";
import { generateCurlCommand } from "~/utils/curl-utils";
import {
  constructFetchHeaders,
  buildUrlEncodedBody,
  buildMultipartFormData,
  serializeMultipartBody,
} from "~/utils/request-utils";
import { processFetchResponse } from "../utils/response-utils.js";
import { inputParametersTemplate } from "~/templates/request-params-template";
import requestBodyTemplate from "~/templates/request-body-template";
import apiCallTemplate from "~/templates/api-response-template";

@customElement("api-request")
export default class ApiRequest extends LitElement {
  @property({ type: Object }) config: any;
  @property({ type: String, attribute: "server-url" }) serverUrl = "";
  @property({ type: Array }) servers: any = [];
  @property({ type: String }) method = "";
  @property({ type: String }) path = "";
  @property({ type: Array }) security: any = [];
  @property({ type: Array }) parameters: any = [];
  @property({ type: Object }) request_body: any;
  @property({ type: Array }) api_keys: any = [];
  @property({ type: Object }) parser: any;
  @property({ type: String }) accept = "";
  @property({ type: String }) callback = "";
  @property({ type: String }) webhook = "";
  @property({ type: String, attribute: "fill-request-fields-with-example" })
  fillRequestFieldsWithExample = "";
  @property({ type: String, attribute: "allow-try" }) allowTry = "true";
  @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false";
  @property({ type: String, attribute: "render-style" }) renderStyle = "view";
  @property({ type: String, attribute: "schema-style" }) schemaStyle = "tree";
  @property({ type: String, attribute: "active-schema-tab" }) activeSchemaTab = "schema";
  @property({
    type: Object,
    converter: {
      fromAttribute: (attr: string) => JSON.parse(attr),
      toAttribute: (prop: any) => JSON.stringify(prop),
    },
    attribute: "active-parameter-schema-tabs",
  })
  activeParameterSchemaTabs: any = {};
  @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999;
  @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded =
    "false";
  @property({ type: String, attribute: "allow-schema-description-expand-toggle" })
  allowSchemaDescriptionExpandToggle = "false";
  @property({ type: String, attribute: "schema-hide-read-only" }) schemaHideReadOnly = "true";
  @property({ type: String, attribute: "schema-hide-write-only" }) schemaHideWriteOnly = "false";
  @property({ type: String, attribute: "fetch-credentials" }) fetchCredentials = "";

  // Internal reactive state
  @state() responseMessage: any = "";
  @state() responseText = "";
  @state() responseHeaders: any = "";
  @state() responseStatus = "success";
  @state() responseUrl = "";
  @state() curlSyntax = "";
  @state() responseIsBlob = false;
  @state() responseBlobType = "";
  @state() responseBlobUrl = "";
  @state() respContentDisposition = "";
  @state() activeResponseTab = "response"; // allowed values: response, headers, curl
  @state() selectedRequestBodyType = "";
  @state() selectedRequestBodyExample = "";
  @state() fileInputKeys: any = {};
  @state() loading = false;
  // Try-bar buttons follow the fields: Fill Example shows only when a field has an example,
  // Clear is enabled only while a field holds a value
  @state() hasExampleToFill = false;
  @state() hasValueToClear = false;

  activeAbortController: any = null;

  constructor() {
    super();
    this.responseMessage = "";
    this.responseStatus = "success";
    this.responseHeaders = "";
    this.responseText = "";
    this.responseUrl = "";
    this.curlSyntax = "";
    this.responseIsBlob = false;
    this.responseBlobType = "";
    this.responseBlobUrl = "";
    this.respContentDisposition = "";
    this.activeResponseTab = "response";
    this.selectedRequestBodyType = "";
    this.selectedRequestBodyExample = "";
    this.activeParameterSchemaTabs = {};
    this.fileInputKeys = {};
    this.loading = false;
  }

  static get styles() {
    return [
      ParamListStyles,
      InputStyles,
      FontStyles,
      FlexStyles,
      BorderStyles,
      TabStyles,
      MicrolighterStyles,
      css`
        *,
        *:before,
        *:after {
          box-sizing: border-box;
        }
        :where(button, input[type='checkbox'], [tabindex='0']):focus-visible {
          box-shadow: var(--focus-shadow);
        }
        :where(input[type='text'], input[type='password'], select, textarea):focus-visible {
          border-color: var(--primary);
        }
        tag-input:focus-within {
          outline: 1px solid;
        }
        .top-gap {
          margin-top: 24px;
        }
        /* The first label under the Request heading belongs to it; no section gap */
        .request-panel > div > .top-gap:first-child,
        .request-panel > div > .request-body-container:first-child > .top-gap {
          margin-top: 4px;
        }

        .textarea {
          min-height: 220px;
          padding: 5px;
          resize: vertical;
          direction: ltr;
        }

        .response-message {
          font-weight: bold;
          text-overflow: ellipsis;

          &.error {
            color: var(--destructive);
          }
          &.success {
            color: var(--blue);
          }
        }

        .file-input-container {
          align-items: flex-start;
          width: 100%;

          /* Each file input spans the full width; its remove button sits inside the box, at the right edge */
          & .input-set {
            position: relative;
            width: 100%;
            display: flex;
            align-items: center;

            &:first-child .file-input-remove-btn {
              display: none;
            }

            &:not(:first-child) input[type='file'] {
              padding-right: 2.25em;
            }
          }

          /* Inset by the file input's border + padding, so it lines up with Choose File */
          & .file-input-add-btn {
            align-self: flex-start;
            margin: 4px 0 0 0.45em;
            font-weight: 400;
          }
        }

        .file-input-remove-btn {
          position: absolute;
          top: 50%;
          right: 0.45em;
          transform: translateY(-50%);
          padding: 0 0.25em;
          font-size: 1.25em;
          line-height: 1;
          color: var(--destructive);
          border: none;
          border-radius: var(--radius);
          background: none;
          cursor: pointer;

          &:hover {
            background: color-mix(in srgb, var(--destructive) 15%, transparent);
          }

          &:focus-visible {
            outline: none;
            box-shadow: var(--focus-shadow);
          }
        }

        .v-tab-btn {
          font-size: var(--font-size-small);
          height: 24px;
          border: none;
          background: none;
          opacity: 0.3;
          cursor: pointer;
          padding: 4px 8px;

          &.active {
            font-weight: bold;
            background: var(--background);
            opacity: 1;
          }
        }

        @container (min-width: 768px) {
          .textarea {
            padding: 8px;
          }
        }

        /*
         * Try bar: server/auth summary on the left, actions on the right. The summary is a two-row
         * label/value list sized like a parameter label (name over type). The bar is its own container,
         * so it reflows by the width it has, not the viewport.
         */
        .try-bar {
          container-type: inline-size;
          margin: 16px 0;
          font-size: var(--font-size-small);
        }
        /* Narrow: summary on top, actions below it on the right */
        .try-layout {
          display: flex;
          flex-direction: column;
          gap: 0.75em;
        }
        .try-info {
          display: grid;
          grid-template-columns: max-content minmax(0, 1fr);
          gap: 0.25em 0.75em;
          margin: 0;
          line-height: 1.35;

          & > dt {
            font-weight: 600;
            color: var(--foreground);
          }
          & > dd {
            margin: 0;
            min-width: 0;
            color: var(--muted-foreground);
            overflow-wrap: anywhere;
          }
        }
        .try-actions {
          display: flex;
          flex-wrap: wrap;
          justify-content: flex-end;
          gap: 0.5em;
        }
        /* Wide: one row; the summary takes the free space and the actions keep their size */
        @container (min-width: 34rem) {
          .try-layout {
            flex-direction: row;
            align-items: center;
            gap: 1.5em;
          }
          .try-info {
            flex: 1;
            min-width: 0;
          }
          .try-actions {
            flex: none;
          }
        }
        /* Very narrow: each label above its value */
        @container (max-width: 20rem) {
          .try-info {
            grid-template-columns: minmax(0, 1fr);
            gap: 0;

            & > dd + dt {
              margin-top: 0.5em;
            }
          }
        }
      `,
      CustomStyles,
    ];
  }

  /** Shared schema & display configurations that affect both parameters and request body */
  get commonConfigDeps() {
    return [
      this.schemaStyle,
      this.schemaExpandLevel,
      this.schemaDescriptionExpanded,
      this.allowSchemaDescriptionExpandToggle,
      this.schemaHideReadOnly,
      this.schemaHideWriteOnly,
      this.fillRequestFieldsWithExample,
      this.renderStyle,
      this.allowTry,
      this.callback,
      this.webhook,
    ];
  }

  /** Dependencies for query/path/header/cookie parameters */
  get parameterGuardDeps() {
    return [
      this.method,
      this.path,
      this.parameters,
      this.activeParameterSchemaTabs,
      ...this.commonConfigDeps,
    ];
  }

  /** Dependencies for the request body template */
  get requestBodyGuardDeps() {
    return [
      this.method,
      this.path,
      this.request_body,
      this.selectedRequestBodyType,
      this.selectedRequestBodyExample,
      this.activeSchemaTab,
      this.fileInputKeys,
      ...this.commonConfigDeps,
    ];
  }

  render() {
    return html`<div
      @input="${this.syncTryBarState}"
      @change="${this.syncTryBarState}"
      class="col regular-font request-panel ${
        "read focused".includes(this.renderStyle) || this.callback === "true"
          ? "read-mode"
          : "view-mode"
      }"
    >
      ${
        this.callback === "true"
          ? html`<h6 class="rapidoc-heading label-heading tiny-title" part="section-request-title heading heading-6">Callback request</h6>`
          : html`<h4 class="rapidoc-heading label-heading req-res-title" part="section-request-title heading heading-4">Request</h4>`
      }
      <div>
        ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "path"))}
        ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "query"))}
        ${guard(this.requestBodyGuardDeps, () => requestBodyTemplate.call(this))}
        ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "header"))}
        ${guard(this.parameterGuardDeps, () => inputParametersTemplate.call(this, "cookie"))}
        ${this.allowTry === "false" ? "" : html`${apiCallTemplate.call(this)}`}
      </div>
    </div>`;
  }

  async updated(changedProperties) {
    if (this.showCurlBeforeTry === "true") {
      if (
        !changedProperties ||
        !changedProperties.has("curlSyntax") ||
        changedProperties.size > 1
      ) {
        this.applyCURLSyntax(this.shadowRoot);
      }
    }
    scheduleHighlight(this.getRootNode()?.host?.shadowRoot || this.shadowRoot);

    if (this.webhook === "true") {
      this.allowTry = "false";
    }
    // After the fields are in the DOM; deferred so the update is not requested mid-update
    queueMicrotask(() => this.syncTryBarState());
  }

  // This method is called before navigation change in focused mode
  async beforeNavigationFocusedMode() {}

  // Called by rapi-doc after navigating to a new endpoint in focused renderStyle.
  // In focused mode, the single <api-request> element instance is reused across operations.
  // We reset selectedRequestBodyType and selectedRequestBodyExample so stale MIME type/example selections
  // from a previous endpoint don't carry over to an endpoint that may not support them.
  // The first valid MIME type will be automatically selected by request-body-template during render.
  async afterNavigationFocusedMode() {
    this.selectedRequestBodyType = "";
    this.selectedRequestBodyExample = "";
    this.clearResponseData();
  }

  // Request-Body Event Handlers
  onSelectExample(e) {
    this.selectedRequestBodyExample = e.target.value;
    const exampleDropdownEl = e.target;
    window.setTimeout(
      (selectEl) => {
        const readOnlyExampleEl = selectEl
          .closest(".example-panel")
          .querySelector(".request-body-param");
        const userInputExampleTextareaEl = selectEl
          .closest(".example-panel")
          .querySelector(".request-body-param-user-input");
        userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;

        const requestPanelEl = this.getRequestPanel({ target: selectEl });
        this.liveCURLSyntaxUpdate(requestPanelEl);
      },
      0,
      exampleDropdownEl,
    );
  }

  onMimeTypeChange(e) {
    this.selectedRequestBodyType = e.target.value;
    const mimeDropdownEl = e.target;
    this.selectedRequestBodyExample = "";
    window.setTimeout(
      (selectEl) => {
        const readOnlyExampleEl = selectEl
          .closest(".request-body-container")
          .querySelector(".request-body-param");
        if (readOnlyExampleEl) {
          const userInputExampleTextareaEl = selectEl
            .closest(".request-body-container")
            .querySelector(".request-body-param-user-input");
          userInputExampleTextareaEl.value = readOnlyExampleEl.innerText;
        }
      },
      0,
      mimeDropdownEl,
    );
  }

  async onFillRequestData(e) {
    const requestPanelEl = e.target.closest(".request-panel");
    const requestPanelInputEls = [
      ...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
    ];
    requestPanelInputEls.forEach((el) => {
      if (el.dataset.example) {
        if (el.tagName.toUpperCase() === "TAG-INPUT") {
          el.value = el.dataset.example.split("~|~");
        } else {
          el.value = el.dataset.example;
        }
      }
    });
    this.syncTryBarState();
  }

  async onClearRequestData(e) {
    const requestPanelEl = e.target.closest(".request-panel");
    const requestPanelInputEls = [
      ...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)"),
    ];
    requestPanelInputEls.forEach((el) => {
      el.value = "";
    });
    this.syncTryBarState();
  }

  // Reads the request fields (the same ones Fill Example and Clear act on) and updates the
  // try-bar flags; setting a flag to its current value schedules no render
  syncTryBarState() {
    const requestPanelEl = this.shadowRoot?.querySelector(".request-panel");
    const inputEls: any[] = requestPanelEl
      ? [...requestPanelEl.querySelectorAll("input, tag-input, textarea:not(.is-hidden)")]
      : [];
    this.hasExampleToFill = inputEls.some((el) => !!el.dataset.example);
    this.hasValueToClear = inputEls.some((el) =>
      Array.isArray(el.value) ? el.value.length > 0 : el.value !== "",
    );
  }

  buildFetchURL(requestPanelEl) {
    const pathParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='path']")];
    const queryParamEls = [...requestPanelEl.querySelectorAll("[data-ptype='query']")];
    const queryParamObjTypeEls = [
      ...requestPanelEl.querySelectorAll("[data-ptype='query-object']"),
    ];

    const pathParams = {};
    pathParamEls.forEach((el) => {
      pathParams[el.dataset.pname] = el.value;
    });

    const queryParams = [];
    queryParamEls.forEach((el) => {
      const isArray = el.dataset.array !== "false";
      const style = el.dataset.paramSerializeStyle || "form";
      const explode =
        el.dataset.paramSerializeExplode !== undefined
          ? el.dataset.paramSerializeExplode === "true"
          : style === "form";
      const allowReserved = el.dataset.paramAllowReserved === "true";

      queryParams.push({
        name: el.dataset.pname,
        value: el.value,
        style,
        explode,
        allowReserved,
        isArray,
        itemType: el.dataset.itemType || "",
      });
    });

    queryParamObjTypeEls.forEach((el) => {
      const style = el.dataset.paramSerializeStyle || "form";
      const explode =
        el.dataset.paramSerializeExplode !== undefined
          ? el.dataset.paramSerializeExplode === "true"
          : style === "form";
      const allowReserved = el.dataset.paramAllowReserved === "true";

      queryParams.push({
        name: el.dataset.pname,
        value: el.value,
        style,
        explode,
        allowReserved,
        isObject: true,
      });
    });

    return constructFetchURL({
      serverUrl: this.serverUrl,
      path: this.path,
      pathParams,
      queryParams,
      apiKeys: this.api_keys,
    });
  }

  buildFetchHeaders(requestPanelEl) {
    const respEl = this.closest(
      ".expanded-req-resp-container, .req-resp-container",
    )?.getElementsByTagName("api-response")[0];
    const headerParamEls = [
      ...requestPanelEl.querySelectorAll("[data-ptype='header'], [data-ptype='header-object']"),
    ];
    const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
    const accept = respEl?.selectedMimeType || this.accept || "";
    const contentType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";

    const headerParams = headerParamEls.map((el) => ({
      name: el.dataset.pname,
      value: el.value,
      explode: el.dataset.paramSerializeExplode === "true",
      isObject: el.dataset.ptype === "header-object",
    }));

    return constructFetchHeaders({
      accept,
      apiKeys: this.api_keys,
      headerParams,
      contentType,
    });
  }

  buildFetchBodyOptions(requestPanelEl) {
    const requestBodyContainerEl = requestPanelEl.querySelector(".request-body-container");
    const fetchOptions: any = {
      method: this.method.toUpperCase(),
    };
    if (requestBodyContainerEl) {
      const requestBodyType = requestBodyContainerEl.dataset.selectedRequestBodyType || "";
      if (requestBodyType.includes("form-urlencoded")) {
        const formUrlDynamicTextAreaEl = requestPanelEl.querySelector(
          "[data-ptype='dynamic-form']",
        );
        if (formUrlDynamicTextAreaEl) {
          fetchOptions.body = buildUrlEncodedBody([], formUrlDynamicTextAreaEl.value);
        } else {
          const formUrlEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-urlencode']")];
          const fields = formUrlEls
            .filter((v: any) => v.type !== "file")
            .map((el: any) => ({
              name: el.dataset.pname,
              value: el.value,
              isArray: el.dataset.array !== "false",
            }));
          fetchOptions.body = buildUrlEncodedBody(fields);
        }
      } else if (requestBodyType.includes("form-data")) {
        const formDataEls = [...requestPanelEl.querySelectorAll("[data-ptype='form-data']")];
        const fields = formDataEls.map((el: any) => ({
          name: el.dataset.pname,
          value: el.value,
          file: el.type === "file" ? el.files?.[0] : undefined,
          isArray: el.dataset.array !== "false",
        }));
        fetchOptions.body = buildMultipartFormData(fields);
        // Content-Type of the parts that need one (application/json for objects, or the spec's encoding)
        const partContentTypes = Object.fromEntries(
          formDataEls
            .filter((el: any) => el.dataset.contentType)
            .map((el: any) => [el.dataset.pname, el.dataset.contentType]),
        );
        if (Object.keys(partContentTypes).length > 0) {
          fetchOptions.partContentTypes = partContentTypes;
        }
      } else if (
        /^audio\/|^image\/|^video\/|^font\/|tar$|zip$|7z$|rtf$|msword$|excel$|\/pdf$|\/octet-stream$/.test(
          requestBodyType,
        )
      ) {
        const bodyParamFileEl: any = requestPanelEl.querySelector(".request-body-param-file");
        if (bodyParamFileEl?.files[0]) {
          fetchOptions.body = bodyParamFileEl.files[0];
        }
      } else if (
        requestBodyType.includes("json") ||
        requestBodyType.includes("xml") ||
        requestBodyType.includes("text")
      ) {
        const exampleTextAreaEl: any = requestPanelEl.querySelector(
          ".request-body-param-user-input",
        );
        if (exampleTextAreaEl?.value) {
          fetchOptions.body = exampleTextAreaEl.value;
        }
      }
    }

    return fetchOptions;
  }

  async onTryClick(e) {
    const tryBtnEl = e?.target;
    const requestPanelEl = tryBtnEl
      ? this.getRequestPanel(e)
      : this.shadowRoot.querySelector(".request-panel");
    const fetchUrl = this.buildFetchURL(requestPanelEl);
    const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
    const reqHeaders = this.buildFetchHeaders(requestPanelEl);
    this.responseUrl = "";
    this.responseHeaders = [];
    this.curlSyntax = this.generateCURLSyntax(fetchUrl, reqHeaders, fetchOptions, requestPanelEl);
    this.responseStatus = "success";
    this.responseIsBlob = false;

    this.respContentDisposition = "";
    if (this.responseBlobUrl) {
      URL.revokeObjectURL(this.responseBlobUrl);
      this.responseBlobUrl = "";
    }
    if (this.fetchCredentials) {
      fetchOptions.credentials = this.fetchCredentials;
    }
    const controller = new AbortController();
    this.activeAbortController = controller;
    const { signal } = controller;
    fetchOptions.headers = reqHeaders;
    const tempRequest = { url: fetchUrl, ...fetchOptions };
    this.dispatchEvent(
      new CustomEvent("before-try", {
        bubbles: true,
        composed: true,
        detail: {
          request: tempRequest,
          controller,
        },
      }),
    );
    const updatedFetchOptions = {
      method: tempRequest.method,
      headers: tempRequest.headers,
      credentials: tempRequest.credentials,
      body: tempRequest.body,
    };
    // FormData can't give a text part a Content-Type, so such bodies are serialized here
    if (tempRequest.body instanceof FormData && tempRequest.partContentTypes) {
      const multipart = serializeMultipartBody(tempRequest.body, tempRequest.partContentTypes);
      const headers = new Headers(tempRequest.headers);
      headers.set("Content-Type", multipart.contentType);
      updatedFetchOptions.headers = headers;
      updatedFetchOptions.body = multipart.body;
    }
    let fetchRequest;
    let fetchResponse;
    let responseClone;
    try {
      this.loading = true;
      this.responseText = "⌛";
      this.responseMessage = "";
      fetchRequest = new Request(tempRequest.url, updatedFetchOptions);
      const startTime = performance.now();
      fetchResponse = await fetch(fetchRequest, { signal });
      const endTime = performance.now();
      responseClone = fetchResponse.clone(); // create a response clone to allow reading response body again (response.json, response.text etc)
      this.responseMessage = html`${fetchResponse.statusText ? `${fetchResponse.statusText}:${fetchResponse.status}` : fetchResponse.status}
        <div style="color:var(--muted-foreground)">Took ${Math.round(endTime - startTime)} milliseconds</div>`;
      this.responseUrl = fetchResponse.url;
      const respHeadersObj = {};
      fetchResponse.headers.forEach((hdrVal, hdr) => {
        respHeadersObj[hdr] = hdrVal;
        this.responseHeaders = `${this.responseHeaders}${hdr}: ${hdrVal}\n`;
      });

      const processed = await processFetchResponse(fetchResponse);
      this.responseText = processed.responseText;
      this.responseIsBlob = processed.responseIsBlob;
      this.responseBlobType = processed.responseBlobType;
      this.responseBlobUrl = processed.responseBlobUrl;
      this.respContentDisposition = processed.respContentDisposition;

      this.dispatchEvent(
        new CustomEvent("after-try", {
          bubbles: true,
          composed: true,
          detail: {
            request: fetchRequest,
            response: responseClone,
            responseHeaders: respHeadersObj,
            responseBody: processed.respJson || processed.respText || processed.respBlob,
            responseStatus: responseClone.ok,
          },
        }),
      );
    } catch (err) {
      if (err.name === "AbortError") {
        this.dispatchEvent(
          new CustomEvent("request-aborted", {
            bubbles: true,
            composed: true,
            detail: {
              err,
              request: fetchRequest,
            },
          }),
        );
        this.responseMessage = "Request Aborted";
        this.responseText = "Request Aborted";
      } else {
        this.dispatchEvent(
          new CustomEvent("after-try", {
            bubbles: true,
            composed: true,
            detail: {
              err,
              request: fetchRequest,
            },
          }),
        );
        if (tempRequest.method?.toUpperCase() === "TRACE") {
          this.responseMessage = `${err.message} (Browser security restricts TRACE requests in fetch. Use the cURL snippet below.)`;
        } else {
          this.responseMessage = `${err.message} (CORS or Network Issue)`;
        }
      }
    } finally {
      this.loading = false;
      this.activeAbortController = null;
    }
  }

  liveCURLSyntaxUpdate(requestPanelEl) {
    this.applyCURLSyntax(requestPanelEl);
  }

  onGenerateCURLClick(e) {
    const requestPanelEl = this.getRequestPanel(e);
    this.applyCURLSyntax(requestPanelEl);
  }

  getRequestPanel(e) {
    return e.target.closest(".request-panel");
  }

  applyCURLSyntax(requestPanelEl) {
    const fetchUrl = this.buildFetchURL(requestPanelEl);
    const fetchOptions = this.buildFetchBodyOptions(requestPanelEl);
    const fetchHeaders = this.buildFetchHeaders(requestPanelEl);

    const newCurlSyntax = this.generateCURLSyntax(
      fetchUrl,
      fetchHeaders,
      fetchOptions,
      requestPanelEl,
    );
    if (this.curlSyntax !== newCurlSyntax) {
      this.curlSyntax = newCurlSyntax;
    }
  }

  generateCURLSyntax(fetchUrl, fetchHeaders, fetchOptions, requestPanelEl) {
    const requestBodyContainerEl = requestPanelEl?.querySelector(".request-body-container");
    const requestBodyType = requestBodyContainerEl?.dataset?.selectedRequestBodyType || "";
    let body = fetchOptions?.body;

    if (!body && requestBodyContainerEl) {
      const exampleTextAreaEl: any = requestPanelEl.querySelector(".request-body-param-user-input");
      if (exampleTextAreaEl?.value) {
        body = exampleTextAreaEl.value;
      }
    }

    return generateCurlCommand({
      method: this.method,
      url: fetchUrl,
      headers: fetchHeaders,
      body,
      contentType: requestBodyType,
      partContentTypes: fetchOptions?.partContentTypes,
    });
  }

  onAddFileInput(fieldName) {
    const currentKeys = this.fileInputKeys[fieldName] ?? [0];
    const nextKey = currentKeys.length > 0 ? Math.max(...currentKeys) + 1 : 0;
    this.fileInputKeys = {
      ...this.fileInputKeys,
      [fieldName]: [...currentKeys, nextKey],
    };
  }

  onRemoveFileInput(fieldName, keyToRemove) {
    const currentKeys = this.fileInputKeys[fieldName] ?? [0];
    this.fileInputKeys = {
      ...this.fileInputKeys,
      [fieldName]: currentKeys.filter((k) => k !== keyToRemove),
    };
  }

  clearResponseData() {
    this.responseUrl = "";
    this.responseHeaders = "";
    this.responseText = "";
    this.responseStatus = "success";
    this.responseMessage = "";
    this.responseIsBlob = false;
    this.responseBlobType = "";
    this.respContentDisposition = "";
    this.fileInputKeys = {};
    if (this.responseBlobUrl) {
      URL.revokeObjectURL(this.responseBlobUrl);
      this.responseBlobUrl = "";
    }
  }

  willUpdate(changedProperties) {
    super.willUpdate?.(changedProperties);
    // Sync configuration options passed down from parent <rapi-doc>
    if (this.config && changedProperties.has("config")) {
      if (this.config.renderStyle) this.renderStyle = this.config.renderStyle;
      if (this.config.schemaStyle) this.schemaStyle = this.config.schemaStyle;
      if (this.config.schemaExpandLevel !== undefined)
        this.schemaExpandLevel = this.config.schemaExpandLevel;
      if (this.config.schemaDescriptionExpanded !== undefined)
        this.schemaDescriptionExpanded = this.config.schemaDescriptionExpanded;
      if (this.config.allowSchemaDescriptionExpandToggle !== undefined)
        this.allowSchemaDescriptionExpandToggle = this.config.allowSchemaDescriptionExpandToggle;
      // In incoming requests, readOnly fields are hidden by default ('true') unless configured to 'never'.
      // For incoming webhooks, the direction is inverted (the external caller sends the payload), so readOnly fields are shown ('false').
      if (this.config.schemaHideReadOnly !== undefined)
        this.schemaHideReadOnly =
          this.config.schemaHideReadOnly === "never"
            ? "false"
            : this.webhook === "true"
              ? "false"
              : "true";
      // In incoming requests, writeOnly fields are shown by default ('false').
      // For webhooks, writeOnly fields are hidden ('true') unless configured to 'never'.
      if (this.config.schemaHideWriteOnly !== undefined)
        this.schemaHideWriteOnly =
          this.config.schemaHideWriteOnly === "never"
            ? "false"
            : this.webhook === "true"
              ? "true"
              : "false";
      if (this.config.fillRequestFieldsWithExample !== undefined)
        this.fillRequestFieldsWithExample = this.config.fillRequestFieldsWithExample;
      if (this.config.allowTry !== undefined) this.allowTry = this.config.allowTry;
      if (this.config.showCurlBeforeTry !== undefined)
        this.showCurlBeforeTry = this.config.showCurlBeforeTry;
      if (this.config.fetchCredentials !== undefined)
        this.fetchCredentials = this.config.fetchCredentials;
      if (this.config.defaultSchemaTab) this.activeSchemaTab = this.config.defaultSchemaTab;
    }
  }

  disconnectedCallback() {
    this.curlSyntax = "";
    if (this.activeAbortController) {
      this.activeAbortController.abort();
      this.activeAbortController = null;
    }
    // Cleanup ObjectURL for the blob data if this component created one
    if (this.responseBlobUrl) {
      URL.revokeObjectURL(this.responseBlobUrl);
      this.responseBlobUrl = "";
    }
    super.disconnectedCallback();
  }
}
