import { html, TemplateResult } from 'lit'
import { property } from 'lit/decorators.js'
import type { IApiSummary } from '@api-client/core/amf/definitions/Api.js'
import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js'
import { ApiDocumentationBase } from './ApiDocumentationBase.js'
import { EventTypes } from '../../events/amf/EventTypes.js'
import { Events } from '../../events/amf/Events.js'
import { ServerChangeEvent, ServerCountChangeEvent } from '../../events/amf/ServerEvents.js'
import { ApiNavigationEvent } from '../../events/amf/NavigationEvents.js'
import { SelectionType, DocumentMeta, ServerType } from '../../amf/types.js'
import { Oauth2Credentials } from '../authorization/types.js'
import RadioElement from '../../md/radio/internals/RadioElement.js'
import '../../define/amf/api-summary.js'
import '../../define/amf/api-operation-document.js'
import '../../define/amf/api-resource-document.js'
import '../../define/amf/api-security-document.js'
import '../../define/amf/api-documentation-document.js'
import '../../define/amf/api-schema-document.js'
import '../../define/amf/api-server-picker.js'
import '../../md/radio/ui-radio.js'
/**
 * A main documentation view for an AMF model representing a sync or an async API.
 *
 * This element works with the [AMF](https://github.com/mulesoft/amf) data model.
 */
export default class ApiDocumentation extends ApiDocumentationBase {
  protected domainTypeValue: SelectionType | undefined

  protected operationIdValue: string | undefined

  protected handleNavigationEventsValue: boolean | undefined

  protected renderedViewValue: string | undefined

  protected apiSummaryValue?: IApiSummary

  protected documentMetaValue?: DocumentMeta

  protected navEventsRegistered: boolean

  /**
   * A property to set to override AMF's model base URI information.
   * @attribute
   */
  @property({ type: String }) accessor baseUri: string | undefined

  /**
   * When set it renders the "try it" button that dispatches the `tryit` event.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor tryItButton: boolean | undefined

  /**
   * When set it renders the "try it" panel next to the operation documentation.
   * Setting this automatically disables the `tryItButton` property.
   *
   * Note, use this only when there's enough space on the screen to render 2 panels side-by-side.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor tryItPanel: boolean | undefined

  /**
   * OAuth2 redirect URI.
   * This value **must** be set in order for OAuth 1/2 to work properly.
   * This is only required in inline mode (`inlineMethods`).
   * @attribute
   */
  @property({ type: String }) accessor redirectUri: string | undefined

  /**
   * When set it renders the URL input above the URL parameters in the HTTP editor.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor httpUrlEditor: boolean | undefined

  /**
   * When set it applies the authorization values to the request dispatched
   * with the API request event.
   * If possible, it applies the authorization values to query parameter or headers
   * depending on the configuration.
   *
   * When the values arr applied to the request the authorization config is kept in the
   * request object, but its `enabled` state is always `false`, meaning other potential
   * processors should ignore this values.
   *
   * If this property is not set then the application hosting this component should
   * process the authorization data and apply them to the request.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor httpApplyAuthorization: boolean | undefined

  /**
   * List of credentials source passed to the HTTP editor
   */
  @property({ type: Array }) accessor httpCredentialsSource: Oauth2Credentials[] | undefined

  /**
   * Optional property to set on the request editor.
   * When true, the server selector is not rendered
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor noServerSelector: boolean | undefined

  /**
   * When set it renders "add custom" item button in the HTTP request editor.
   * If the element is to be used without AMF model this should always
   * be enabled. Otherwise users won't be able to add a parameter.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor httpAllowCustom: boolean | undefined

  /**
   * Optional property to set on the request editor.
   * If true, the server selector custom base URI option is rendered
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor allowCustomBaseUri: boolean | undefined

  /**
   * The URI of the server currently selected in the server selector
   * @attribute
   */
  @property({ type: String }) accessor serverValue: string | undefined

  /**
   * The type of the server currently selected in the server selector
   * @attribute
   */
  @property({ type: String }) accessor serverType: string | undefined

  /**
   * The mime type of the currently selected schema.
   * @attribute
   */
  @property({ type: String }) accessor schemaMimeType: string | undefined

  serversCount?: number

  /**
   * The type of the selected domain item.
   * @attribute
   */
  get domainType(): SelectionType | undefined {
    return this.domainTypeValue
  }

  @property({ type: String, reflect: true })
  set domainType(value: SelectionType | undefined) {
    const old = this.domainTypeValue
    if (old === value) {
      return
    }
    this.domainTypeValue = value
    this.requestUpdate('domainType', old)
    if (value) {
      this.processDebounce()
    }
  }

  /**
   * The domain id of the currently rendered API operation.
   * When selecting an operation the `domainId` is the id if the parent endpoint.
   * @attribute
   */
  get operationId(): string | undefined {
    return this.operationIdValue
  }

  @property({ type: String, reflect: true })
  set operationId(value: string | undefined) {
    const old = this.operationIdValue
    if (old === value) {
      return
    }
    this.operationIdValue = value
    this.requestUpdate('operationId', old)
    if (value) {
      this.processDebounce()
    }
  }

  /**
   * By default application hosting the element must set `domainId` and
   * `domainType` properties. When using `api-navigation` element
   * by setting this property the element listens for navigation events
   * and updates the state
   * @attribute
   */
  get handleNavigationEvents(): boolean | undefined {
    return this.handleNavigationEventsValue
  }

  @property({ type: Boolean, reflect: true })
  set handleNavigationEvents(value: boolean | undefined) {
    const old = this.handleNavigationEventsValue
    if (old === value) {
      return
    }
    this.handleNavigationEventsValue = value
    if (value) {
      this.registerNavigationEvents()
    } else {
      this.unregisterNavigationEvents()
    }
  }

  /**
   * Former `effectiveBaseUri`.
   * @returns The URI for the API defined by the `baseUri` property or the `serverValue`.
   */
  get apiBaseUri(): string | undefined {
    const { baseUri, serverValue } = this
    return baseUri || serverValue
  }

  /**
   * @deprecated Use `apiBaseUri` instead.
   */
  get effectiveBaseUri(): string | undefined {
    return this.apiBaseUri
  }

  /** The domain type of the rendered view. */
  get renderedView(): string | undefined {
    return this.renderedViewValue
  }

  get renderSelector(): boolean {
    const { domainType, serversCount = 0, allowCustomBaseUri } = this
    const isOperationOrEndpoint = !!domainType && ['operation', 'resource'].includes(domainType)
    const moreThanOneServer = serversCount >= 2
    if (isOperationOrEndpoint) {
      return allowCustomBaseUri || moreThanOneServer
    }
    return false
  }

  /**
   * This is a computed value from the AMF model.
   * @returns true when whe currently loaded API is an async API.
   */
  get isAsync(): boolean {
    const { documentMeta } = this
    if (!documentMeta) {
      return false
    }
    return documentMeta.isAsync
  }

  /**
   * @returns The mime type of the schema that is being rendered.
   */
  get schemaMime(): string | undefined {
    const { schemaMimeType } = this
    if (schemaMimeType) {
      return schemaMimeType
    }
    const summary = this.apiSummaryValue
    if (!summary) {
      return undefined
    }
    const { accepts = [] } = summary
    if (!accepts.length) {
      return undefined
    }
    return accepts[0]
  }

  get documentMeta(): DocumentMeta | undefined {
    return this.documentMetaValue
  }

  constructor() {
    super()

    this.navigationHandler = this.navigationHandler.bind(this)
    this.navEventsRegistered = false
  }

  override connectedCallback(): void {
    super.connectedCallback()
    this.processDebounce()
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    if (this.navEventsRegistered) {
      this.unregisterNavigationEvents()
    }
  }

  /**
   * Registers the api navigation event listener handler
   * on the window object.
   */
  protected registerNavigationEvents(): void {
    this.navEventsRegistered = true
    window.addEventListener(EventTypes.Navigation.apiNavigate, this.navigationHandler as EventListener)
  }

  /**
   * Removes event listener from window object for the API navigation event.
   */
  protected unregisterNavigationEvents(): void {
    this.navEventsRegistered = false
    window.removeEventListener(EventTypes.Navigation.apiNavigate, this.navigationHandler as EventListener)
  }

  /**
   * Handler for the API navigation event.
   *
   * Note, when the current type is set to `operation` then the `operationId` is
   * set instead of `domainId`, which is set to the parent endpoint id.
   */
  protected navigationHandler(e: ApiNavigationEvent): void {
    const { domainId, domainType, parentId, passive } = e.detail
    if (passive === true) {
      return
    }
    this.domainType = domainType
    if (domainType === 'operation') {
      this.operationId = domainId
      this.domainId = parentId
    } else {
      this.operationId = undefined
      this.domainId = domainId
    }
    this.processGraph()
  }

  override async processGraph(): Promise<void> {
    this.schemaMimeType = undefined
    await this.queryDocumentMeta()
    const { documentMeta } = this
    if (!documentMeta) {
      this.requestUpdate()
      return
    }
    if (documentMeta.isApi) {
      await this.queryApiSummary()
      this.processApiSpecSelection()
      return
    }
    if (documentMeta.isLibrary) {
      this.processLibrarySelection()
      return
    }
    const { isFragment, types } = documentMeta

    if (isFragment) {
      let type: SelectionType | undefined
      if (types.includes(AmfNamespace.aml.vocabularies.security.SecuritySchemeFragment)) {
        type = 'security'
      } else if (types.includes(AmfNamespace.aml.vocabularies.apiContract.UserDocumentationFragment)) {
        type = 'documentation'
      } else if (types.includes(AmfNamespace.aml.vocabularies.shapes.DataTypeFragment)) {
        type = 'schema'
      }
      this.processFragment(type)
      return
    }

    //
    // partial models
    //

    if (types.includes(AmfNamespace.aml.vocabularies.apiContract.EndPoint)) {
      this.processEndpointPartial()
      return
    }

    let type: SelectionType | undefined
    if (types.includes(AmfNamespace.aml.vocabularies.core.CreativeWork)) {
      type = 'documentation'
    } else if (types.includes(AmfNamespace.aml.vocabularies.security.SecurityScheme)) {
      type = 'security'
    } else if (types.includes(AmfNamespace.w3.shacl.Shape)) {
      type = 'schema'
    }
    this.processPartial(type)
  }

  /**
   * Reads the currently loaded document meta data from the store.
   */
  async queryDocumentMeta(): Promise<void> {
    this.documentMetaValue = undefined
    try {
      const info = await Events.Api.documentMeta(this)
      this.documentMetaValue = info
    } catch (e) {
      const ex = e as Error
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for the document meta data: ${ex.message}`, this.localName)
    }
  }

  /**
   * Reads the API summary.
   * Called only when the currently loaded document is an API.
   */
  async queryApiSummary(): Promise<void> {
    try {
      const info = await Events.Api.summary(this)
      this.apiSummaryValue = info
    } catch (e) {
      this.apiSummaryValue = undefined
      const ex = e as Error
      Events.Telemetry.exception(this, ex.message, false)
      Events.Reporting.error(this, ex, `Unable to query for the API summary data: ${ex.message}`, this.localName)
    }
  }

  /**
   * Processes selection for the web API data model. It ignores the input if
   * `domainId` or `domainType` is not set.
   */
  protected processApiSpecSelection(): void {
    const { domainId, tryItPanel } = this
    let { domainType } = this
    if (!domainId || !domainType) {
      // Not all required properties were set.
      return
    }
    if (domainType === 'operation' && tryItPanel) {
      domainType = 'resource'
    }
    this.renderedViewValue = domainType
    this.requestUpdate()
  }

  /**
   * Processes selection for a library data model. It ignores the input if
   * `domainId` or `domainType` is not set.
   */
  protected processLibrarySelection(): void {
    const { domainId, domainType } = this
    if (!domainId || !domainType) {
      // Not all required properties were set.
      return
    }
    this.renderedViewValue = domainType
    this.requestUpdate()
  }

  /**
   * Processes fragment model and sets current selection and the model.
   *
   * @param domainType The selected domain type.
   */
  protected processFragment(domainType?: SelectionType): void {
    this.domainIdValue = this.documentMeta?.encodesId
    this.renderedViewValue = domainType
    this.requestUpdate()
  }

  /**
   * Sets the partial model to be rendered.
   *
   * @param domainType The domain type representing the partial model.
   */
  protected processPartial(domainType?: SelectionType): void {
    this.renderedViewValue = domainType
    this.requestUpdate()
  }

  /**
   * Processes endpoint data from partial model definition.
   * It sets models that are used by the docs.
   *
   * If `selected` or `selectedType` is not set then it automatically selects
   */
  protected processEndpointPartial(): void {
    const { tryItPanel } = this
    let { domainType } = this
    if (!domainType || tryItPanel) {
      domainType = 'resource'
    }
    if (!['operation', 'resource'].includes(domainType)) {
      domainType = 'resource'
    }
    this.renderedViewValue = domainType
    this.requestUpdate()
  }

  protected serversCountHandler(e: ServerCountChangeEvent): void {
    this.serversCount = e.detail.value
  }

  protected serverChangeHandler(e: ServerChangeEvent): void {
    this.serverValue = e.detail.value
    this.serverType = e.detail.type
  }

  protected mediaTypeSelectHandler(e: Event): void {
    const node = e.target as RadioElement
    const { value } = node
    this.schemaMimeType = value
  }

  override render(): TemplateResult {
    return html` ${this.serverSelectorTemplate()} ${this.modelTemplate()}`
  }

  /**
   * @returns The template for the server selector.
   */
  protected serverSelectorTemplate(): TemplateResult | string {
    if (this.noServerSelector) {
      return ''
    }
    const { serverType, serverValue, allowCustomBaseUri, renderSelector, domainId, domainType } = this
    const id = domainType === 'operation' ? this.operationId : domainId
    return html` <api-server-picker
      class="server-selector"
      .domainId="${id}"
      .domainType="${domainType}"
      .value="${serverValue}"
      .type="${serverType as ServerType}"
      ?hidden="${!renderSelector}"
      ?custom="${allowCustomBaseUri}"
      auto
      @serverscountchanged="${this.serversCountHandler}"
      @apiserverchanged="${this.serverChangeHandler}"
    >
      <slot name="custom-base-uri" slot="custom-base-uri"></slot>
    </api-server-picker>`
  }

  /**
   * @returns The template for the server selector.
   */
  protected modelTemplate(): TemplateResult | string {
    switch (this.renderedViewValue) {
      case 'summary':
        return this.summaryTemplate()
      case 'security':
        return this.securityTemplate()
      case 'documentation':
        return this.documentationTemplate()
      case 'schema':
        return this.schemaTemplate()
      case 'resource':
      case 'operation':
        return this.resourceTemplate()
      default:
        return ''
    }
  }

  /**
   * @returns The template for the API summary page.
   */
  protected summaryTemplate(): TemplateResult | string {
    const { baseUri } = this
    return html` <api-summary .baseUri="${baseUri}"></api-summary>`
  }

  /**
   * @returns The template for the API security definition page.
   */
  protected securityTemplate(): TemplateResult | string {
    const { domainId } = this
    return html`<api-security-document .domainId="${domainId}" settingsOpened></api-security-document>`
  }

  /**
   * @returns The template for the RAML's documentation page.
   */
  protected documentationTemplate(): TemplateResult | string {
    const { domainId } = this
    return html`<api-documentation-document .domainId="${domainId}"></api-documentation-document>`
  }

  /**
   * @returns The template for the API schema page.
   */
  protected schemaTemplate(): TemplateResult | string {
    const { schemaMime, domainId } = this
    return html` ${this.schemaMediaSelectorTemplate()}
      <api-schema-document .domainId="${domainId}" .mimeType="${schemaMime}" forceExamples></api-schema-document>`
  }

  /**
   * @returns The template for the API endpoint page.
   */
  protected resourceTemplate(): TemplateResult | string {
    const { domainId, operationId, isAsync } = this
    return html`<api-resource-document
      .domainId="${domainId}"
      .operationId="${operationId}"
      .redirectUri="${this.redirectUri}"
      .serverType="${this.serverType}"
      .serverValue="${this.serverValue}"
      .baseUri="${this.apiBaseUri}"
      ?tryItButton="${this.tryItButton}"
      ?tryItPanel="${this.tryItPanel}"
      ?httpUrlEditor="${this.httpUrlEditor}"
      httpNoServerSelector
      ?asyncApi="${isAsync}"
    ></api-resource-document>`
  }

  /**
   * This is a part of schema rendering.
   * When the current API defines the media types then this shows the selector
   * to render examples with a specific mime.
   */
  protected schemaMediaSelectorTemplate(): TemplateResult | string {
    const { documentMeta } = this
    // only APIs have top level media types (?)
    const summary = this.apiSummaryValue
    if (!summary || !documentMeta || !documentMeta.isApi) {
      return ''
    }
    const { accepts = [] } = summary
    if (accepts.length < 2) {
      // if there's a single mime then we render the one we have. No selector needed.
      return ''
    }
    const mimeType = this.schemaMimeType || accepts[0]
    return html`
      <div class="media-type-selector">
        <span id="schema-mime">Schema content type</span>
        ${accepts.map(
          (item) => html`
            <label class="label-large">
              <ui-radio
                class="schema-toggle"
                name="schemaMime"
                value="${item}"
                .checked="${item === mimeType}"
                @change="${this.mediaTypeSelectHandler}"
              ></ui-radio>
              ${item}
            </label>
          `
        )}
      </div>
    `
  }
}
