import { css, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
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 MicrolighterStyles from "~/styles/microlighter-styles";
import TabStyles from "~/styles/tab-styles";
import NavStyles from "~/styles/nav-styles";
import InfoStyles from "~/styles/info-styles";

import EndpointStyles from "~/styles/endpoint-styles";
import ThemeTokens, {
  DEFAULT_THEME,
  applyCustomTheme,
  isValidHexColor,
  normalizeTheme,
} from "~/styles/theme-tokens";
import { rapidocApiKey } 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 { applyApiKey, onClearAllApiKeys } from "~/templates/security-scheme-template";
import { setApiServer } from "~/templates/server-template";
import "~/utils/markdown-utils";
import { AutoColorSchemeController } from "~/utils/auto-color-scheme";

const paramsConverter = {
  fromAttribute: (attr: string | null) => {
    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: any) => {
    if (typeof prop === "object" && prop !== null) {
      return JSON.stringify(prop);
    }
    return prop;
  },
};

@customElement("rapi-doc-mini")
export default class RapiDocMini extends LitElement {
  // Spec
  @property({ type: String, attribute: "spec-url" }) specUrl = "";
  @property({ type: String, attribute: "sort-endpoints-by" }) sortEndpointsBy = "path";

  // UI Layouts
  @property({ type: String }) layout = "row";
  @property({ type: String, attribute: "paths-expanded" }) pathsExpanded: any = "false";
  @property({ type: String, attribute: "default-schema-tab" }) defaultSchemaTab = "example";
  @property({ type: String, attribute: "response-area-height" }) responseAreaHeight = "300px";
  @property({ type: String, attribute: "show-summary-when-collapsed" }) showSummaryWhenCollapsed =
    "true";
  @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";

  // 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";

  @property({ type: String, attribute: "allow-try" }) allowTry = "true";
  @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false";

  // 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";

  // 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;

  isMini: boolean;
  _specLoadSeq: number;
  updateRoute: string;
  renderStyle: string;
  showHeader: string;
  allowAdvancedSearch: string;
  resolvedSpec: any;
  selectedServer: any;
  loadFailed: boolean;
  sortTags: any;
  generateMissingTags: any;
  sortSchemas: any;
  allowSchemaDescriptionExpandToggle: any;

  constructor() {
    super();
    this.isMini = true;
    // Spec loads carry a token; only the latest load may apply its result (see loadSpec)
    this._specLoadSeq = 0;
    this.updateRoute = "false";
    this.renderStyle = "view";
    this.showHeader = "false";
    this.allowAdvancedSearch = "false";
    this.loadFailed = false;
  }

  static get styles() {
    return [
      ThemeTokens,
      FontStyles,
      InputStyles,
      FlexStyles,
      AuthStyles,
      ParamListStyles,
      EndpointStyles,
      MicrolighterStyles,
      TabStyles,
      NavStyles,
      InfoStyles,
      css`
        :host {
          all: initial;
          /* Same crisp text rendering as <rapi-doc> (see layout-styles.js) */
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          display: flex;
          flex-direction: column;
          min-width: 360px;
          width: 100%;
          height: 100%;
          margin: 0;
          padding: 0;
          overflow: hidden;
          letter-spacing: normal;
          color: var(--foreground);
          background: var(--background);
          font-family: var(--font-sans);
          container-type: inline-size;
        }

        @container (min-width: 768px) {
          .only-large-screen {
            display: block;
          }
          .only-large-screen-flex {
            display: flex;
          }
        }
      `,
    ];
  }

  // Startup
  connectedCallback() {
    super.connectedCallback();

    if (
      !this.showSummaryWhenCollapsed ||
      !"true, false,".includes(`${this.showSummaryWhenCollapsed},`)
    ) {
      this.showSummaryWhenCollapsed = "true";
    }
    if (!this.layout || !"row, column,".includes(`${this.layout},`)) {
      this.layout = "row";
    }
    if (!this.schemaStyle || !"tree, table,".includes(`${this.schemaStyle},`)) {
      this.schemaStyle = "tree";
    }
    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.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";
    }
    this.pathsExpanded = this.pathsExpanded === "true";
    if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) {
      this.schemaExpandLevel = 99999;
    }
    if (
      !this.schemaDescriptionExpanded ||
      !"true, false,".includes(`${this.schemaDescriptionExpanded},`)
    ) {
      this.schemaDescriptionExpanded = "false";
    }
    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 = "300px";
    }

    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.sortTags || !"true, false,".includes(`${this.sortTags},`)) {
      this.sortTags = "false";
    }
    if (!this.sortEndpointsBy || !"method, path, summary,".includes(`${this.sortEndpointsBy},`)) {
      this.sortEndpointsBy = "path";
    }

    if (!this.matchType || !"includes regex".includes(this.matchType)) {
      this.matchType = "includes";
    }
    if (!this.matchPaths) {
      this.matchPaths = "";
    }
    if (!this.removeEndpointsWithBadgeLabelAs) {
      this.removeEndpointsWithBadgeLabelAs = "";
    }

    if (
      !this.allowSchemaDescriptionExpandToggle ||
      !"true, false,".includes(`${this.allowSchemaDescriptionExpandToggle},`)
    ) {
      this.allowSchemaDescriptionExpandToggle = "true";
    }
    if (
      !this.fetchCredentials ||
      !"omit, same-origin, include,".includes(`${this.fetchCredentials},`)
    ) {
      this.fetchCredentials = "";
    }
  }

  disconnectedCallback() {
    if (this.mockServer === "true") {
      disableMockServer(this);
    }
    super.disconnectedCallback();
  }

  render() {
    return mainBodyTemplate.call(this, true, this.pathsExpanded);
  }

  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")) {
      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("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,
      });
    }
  }

  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 () => {
          await this.loadSpec(newVal);
        }, 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 === "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: any) => 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") as any).value);
  }

  // Public Method
  async loadSpec(specUrl) {
    if (!specUrl) {
      return;
    }
    // Each load gets a token; a load whose token is no longer current never applies its result
    const token = ++this._specLoadSeq;
    try {
      this.resolvedSpec = {
        specLoadError: false,
        isSpecLoading: true,
        tags: [],
      };
      this.loading = true;
      this.loadFailed = false;
      this.requestUpdate();
      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,
      );
      if (token !== this._specLoadSeq) {
        return; // a newer load started while this one was parsing
      }
      this.loading = false;
      this.afterSpecParsedAndValidated(spec, token);
    } catch (err: any) {
      if (token !== this._specLoadSeq) {
        return;
      }
      this.loading = false;
      this.loadFailed = true;
      this.resolvedSpec = null;
      console.error(`RapiDoc: Unable to resolve the API spec..  ${err.message}`);
    }
  }

  // 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);
  }

  async afterSpecParsedAndValidated(spec, token = this._specLoadSeq) {
    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: any) => 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,
      });
    }
  }

  // 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) {
          gotoEl.scrollIntoView({ behavior: "auto", block: "start" });
        }
      }
    }
  }
}
