import { html, nothing, TemplateResult } from 'lit'
import { type IRequestLog, RequestLog as CoreRequestLog } from '@api-client/core/models/RequestLog.js'
import { RequestTime } from '@api-client/core/models/RequestTime.js'
import type { ResponseRedirect } from '@api-client/core/models/ResponseRedirect.js'
import { ErrorResponse } from '@api-client/core/models/ErrorResponse.js'
import type { Response } from '@api-client/core/models/Response.js'
import { Headers } from '@api-client/core/lib/headers/Headers.js'
import type { AssertionResult } from '@api-client/core/models/http-flows/HttpAssertion.js'
import type { AssertionError } from '@api-client/core/models/http-flows/AssertionError.js'
import { property, state } from 'lit/decorators.js'
import { statusTemplate } from './HttpStatus.js'
import ApiElement from '../ApiElement.js'
import { TabSelectionDetail } from '../../md/tabs/internals/Tabs.js'
import '../../define/http/http-log-headers.js'
import '../../define/http/http-log-body.js'
import '../../define/http/http-log-timings.js'
import '../../md/tabs/ui-tab.js'
import '../../md/tabs/ui-tabs.js'
import '../../md/divider/ui-divider.js'

export enum RequestLogView {
  headers = 'headers',
  payload = 'payload',
  preview = 'preview',
  response = 'response',
  timings = 'timings',
  redirects = 'redirects',
  assertions = 'assertions',
}

/**
 * An element that renders the view for a core's `IRequestLog`.
 */
export default class RequestLog extends ApiElement {
  protected httpLogInternal?: IRequestLog | CoreRequestLog

  /**
   * This property is set when the `httpLog` is set.
   * It always represent an instance of the `RequestLog` (or undefined) regardless of the input format.
   */
  protected effectiveLog?: CoreRequestLog

  /**
   * The request log to render. When not set it renders nothing.
   */
  get httpLog(): IRequestLog | CoreRequestLog | undefined {
    return this.httpLogInternal
  }

  @property({ type: Object })
  set httpLog(value: IRequestLog | CoreRequestLog | undefined) {
    const old = this.httpLogInternal
    if (old === value) {
      return
    }
    this.httpLogInternal = value
    if (value) {
      if (typeof (value as CoreRequestLog).toJSON === 'function') {
        this.effectiveLog = value as CoreRequestLog
      } else {
        this.effectiveLog = new CoreRequestLog(value as IRequestLog)
      }
    } else {
      this.effectiveLog = undefined
    }
    this.processPayload()
    this.requestUpdate()
  }

  /**
   * Currently rendered view.
   * @attribute
   */
  @property({ type: String }) accessor view: RequestLogView = RequestLogView.headers

  /**
   * Returns `true` when the request has a payload message.
   */
  get hasRequestPayload(): boolean {
    const { effectiveLog } = this
    if (!effectiveLog) {
      return false
    }
    const { request } = effectiveLog
    if (!request) {
      return false
    }
    if (['get', 'head'].includes(request.method.toLowerCase())) {
      return false
    }
    return !!request.payload
  }

  /**
   * Returns `true` when the response has a payload message.
   */
  get hasResponsePayload(): boolean {
    const { effectiveLog } = this
    if (!effectiveLog) {
      return false
    }
    const { response } = effectiveLog
    if (!response) {
      return false
    }
    return !!response.payload
  }

  get hasResponse(): boolean {
    const { effectiveLog } = this
    if (!effectiveLog) {
      return false
    }
    const { response } = effectiveLog
    return !!response
  }

  @state() protected accessor isErrorResponse = false

  @state() protected accessor timingsData: RequestTime[] | undefined

  @state() protected accessor redirectsData: ResponseRedirect[] | undefined

  /**
   * Reads the payload to the original format for both the request and the response.
   */
  protected async processPayload(): Promise<void> {
    const { effectiveLog } = this
    if (!effectiveLog) {
      this.isErrorResponse = false
      this.timingsData = undefined
      this.redirectsData = undefined
      return
    }
    const { response } = effectiveLog
    if (!response) {
      this.isErrorResponse = false
      this.timingsData = undefined
      this.redirectsData = undefined
    } else {
      this.isErrorResponse = ErrorResponse.isErrorResponse(response)
      // computes list of redirects.
      if (!this.isErrorResponse) {
        const r = response as Response
        const timings: RequestTime[] = []
        const redirects: ResponseRedirect[] = []
        if (Array.isArray(effectiveLog.redirects)) {
          effectiveLog.redirects.forEach((rdr) => {
            redirects.push(rdr)
            if (rdr.timings) {
              timings.push(rdr.timings)
            }
          })
        }
        if (r.timings) {
          timings.push(r.timings)
        }
        this.timingsData = timings
        this.redirectsData = redirects
      }
    }
  }

  protected handleViewSelect(e: CustomEvent<TabSelectionDetail>): void {
    const key = e.detail.item.getAttribute('aria-controls') as RequestLogView | undefined
    if (!key) {
      return
    }
    this.view = key
  }

  protected override render(): TemplateResult {
    const { effectiveLog } = this
    return html` ${this.renderTabs(effectiveLog)} ${this.renderContent(effectiveLog)} `
  }

  protected renderTabs(effectiveLog?: CoreRequestLog): TemplateResult {
    const { view: s = 'headers' } = this
    return html`
      <ui-tabs @select="${this.handleViewSelect}">
        <ui-tab
          .selected="${s === 'headers'}"
          aria-controls="headers"
          id="headers-tab"
          title="Request and response header"
          >Headers</ui-tab
        >
        <ui-tab
          ?hidden="${!this.hasRequestPayload}"
          .selected="${s === 'payload'}"
          aria-controls="payload"
          id="payload-tab"
          title="Request payload set to server"
          >Payload</ui-tab
        >
        <ui-tab
          ?hidden="${!this.hasResponsePayload}"
          .selected="${s === 'preview'}"
          aria-controls="preview"
          id="preview-tab"
          title="Formatted view of the response"
          >Preview</ui-tab
        >
        <ui-tab
          .selected="${s === 'response'}"
          aria-controls="response"
          id="response-tab"
          title="Unformatted view of the response"
          >Response</ui-tab
        >
        <ui-tab .selected="${s === 'timings'}" aria-controls="timings" id="timings-tab" title="Request detailed timings"
          >Timings</ui-tab
        >
        <ui-tab
          .selected="${s === 'redirects'}"
          aria-controls="redirects"
          id="redirects-tab"
          title="Detailed redirect information"
          >Redirects</ui-tab
        >
        <ui-tab
          .selected="${s === 'assertions'}"
          aria-controls="assertions"
          id="assertions-tab"
          title="HTTP assertions result"
          >Assertions ${this.renderAssertionsBadge(effectiveLog)}</ui-tab
        >
      </ui-tabs>
    `
  }

  protected renderAssertionsBadge(effectiveLog?: CoreRequestLog): TemplateResult | typeof nothing {
    if (!effectiveLog) {
      return nothing
    }
    const { assertions = [] } = effectiveLog
    const failed = assertions.filter((i) => !i.ok)
    if (!failed.length) {
      return nothing
    }
    return html` <span class="badge large-label tab">${failed.length}</span> `
  }

  protected renderContent(effectiveLog?: CoreRequestLog): TemplateResult {
    const { view = RequestLogView.headers } = this
    if (!effectiveLog) {
      return html`<div class="empty-info">No data available.</div>`
    }
    let tpl: TemplateResult
    switch (view) {
      case RequestLogView.payload:
        tpl = this.renderPayload(effectiveLog)
        break
      case RequestLogView.preview:
        tpl = this.renderPreview(effectiveLog)
        break
      case RequestLogView.response:
        tpl = this.renderResponse(effectiveLog)
        break
      case RequestLogView.timings:
        tpl = this.renderTimings()
        break
      case RequestLogView.redirects:
        tpl = this.renderRedirects()
        break
      case RequestLogView.assertions:
        tpl = this.renderAssertions(effectiveLog)
        break
      default:
        tpl = this.renderHeaders(effectiveLog)
    }
    return tpl
  }

  /**
   * Renders basic information about the request:
   * - request method & URL
   * - request headers
   * - response headers
   */
  protected renderHeaders(info: CoreRequestLog): TemplateResult {
    return html`
      <http-log-headers
        .httpLog="${info}"
        role="tabpanel"
        aria-labelledby="headers-tab"
        id="headers"
      ></http-log-headers>
    `
  }

  /**
   * @returns The template for the "raw" response body view.
   */
  protected renderPayload(info: CoreRequestLog): TemplateResult {
    if (!info) {
      return html``
    }
    const { request } = info
    if (!request) {
      return this._noDataTemplate('payload-tab', 'payload')
    }
    const parser = new Headers(request.headers)
    const mime = parser.get('content-type')
    return html`
      <http-log-body
        .payload="${request.payload}"
        .contentType="${mime}"
        role="tabpanel"
        aria-labelledby="payload-tab"
        id="payload"
      ></http-log-body>
    `
  }

  /**
   * @returns The template for the parsed response body view.
   */
  protected renderPreview(info: CoreRequestLog): TemplateResult {
    if (!info) {
      return html``
    }
    const { response } = info
    if (!response) {
      return html`<p aria-labelledby="preview-tab" id="preview" role="tabpanel">No response data</p>`
    }
    const parser = new Headers(response.headers)
    const mime = parser.get('content-type')
    return html`
      <http-log-body
        aria-labelledby="preview-tab"
        id="preview"
        .payload="${response.payload}"
        .contentType="${mime}"
        role="tabpanel"
      ></http-log-body>
    `
  }

  protected renderResponse(info: CoreRequestLog): TemplateResult {
    if (!info) {
      return html``
    }
    const { response } = info
    if (!response) {
      return this._noDataTemplate('response-tab', 'response')
    }
    const parser = new Headers(response.headers)
    const mime = parser.get('content-type')
    return html`
      <http-log-body
        aria-labelledby="response-tab"
        id="response"
        .payload="${response.payload}"
        .contentType="${mime}"
        raw
        role="tabpanel"
      ></http-log-body>
    `
  }

  protected renderTimings(): TemplateResult {
    const { timingsData } = this
    if (!timingsData || !timingsData.length) {
      return this._noDataTemplate('timings-tab', 'timings')
    }
    return html`<http-log-timings
      aria-labelledby="timings-tab"
      id="timings"
      .timings="${timingsData}"
      role="tabpanel"
    ></http-log-timings>`
  }

  protected renderRedirects(): TemplateResult {
    const { redirectsData } = this
    if (!redirectsData || !redirectsData.length) {
      return this._noDataTemplate('redirects-tab', 'redirects')
    }
    return html`
      <div role="tabpanel" aria-labelledby="redirects-tab" id="redirects">
        ${redirectsData.map((item, index) => this.renderRedirect(item, index))}
      </div>
    `
  }

  protected renderRedirect(item: ResponseRedirect, index: number): TemplateResult {
    const { url, response } = item
    if (!response) {
      return html`
        <div class="status-row">
          <div class="status-label text">#<span>${index + 1}</span></div>
          <div class="redirect-value">This redirect has no response data</div>
        </div>
      `
    }
    const headers = new Headers(response.headers)
    const mime = headers.get('content-type')
    return html`
      <div class="status-row">
        <div class="status-label text">#<span>${index + 1}</span></div>
        <div class="redirect-value">
          <div class="redirect-code">${statusTemplate(response.status, response.statusText)}</div>
          <div class="redirect-location">
            <a href="${url}" class="auto-link">${url}</a>
          </div>
          ${this.renderHeadersRaw(headers)}
          ${response.payload
            ? html`<http-log-body
                .payload="${response.payload}"
                .contentType="${mime}"
                role="tabpanel"
              ></http-log-body>`
            : ''}
        </div>
      </div>
    `
  }

  protected renderHeadersRaw(header: Headers): TemplateResult | string {
    const templates: TemplateResult[] = []
    header.forEach((value, name) => {
      templates.push(html`
        <li>
          <span class="header-name">${name}</span>
          <span class="header-value">${value}</span>
        </li>
      `)
    })
    if (!templates.length) {
      return ''
    }
    return html`
      <ul class="headers">
        ${templates}
      </ul>
    `
  }

  protected _noDataTemplate(arialLabelledBy: string, id: string): TemplateResult {
    return html`
      <div class="no-data" aria-labelledby="${arialLabelledBy}" id="${id}" role="tabpanel">No data available.</div>
    `
  }

  protected renderAssertions(info: CoreRequestLog): TemplateResult {
    if (!info) {
      return html``
    }
    const { assertions } = info
    if (!Array.isArray(assertions) || !assertions.length) {
      return html`<p aria-labelledby="assertions-tab" id="assertions" role="tabpanel" class="no-data">
        No assertions defined.
      </p>`
    }
    return html`
      <section aria-label="Assertions" aria-labelledby="assertions-tab" id="assertions" role="tabpanel">
        ${assertions.map(
          (item, index) => html`
            ${this.renderAssertion(item)}
            ${index + 1 === assertions.length ? '' : html`<ui-divider type="inset"></ui-divider>`}
          `
        )}
      </section>
    `
  }

  renderAssertion(item: AssertionResult): TemplateResult {
    const { description, ok, error } = item
    if (!ok) {
      return this.renderFailedAssertion(description, error as AssertionError)
    }
    return this.renderOkAssertion(description)
  }

  renderOkAssertion(description: string): TemplateResult {
    return html`
      <div class="assertion success">
        <ui-icon role="presentation" icon="check" class="assertion-icon primary-text"></ui-icon>
        <span class="label-large">${description}</span>
      </div>
    `
  }

  renderFailedAssertion(description: string, error: AssertionError): TemplateResult {
    return html`
      <div class="assertion fail">
        <ui-icon role="presentation" icon="cancel" class="assertion-icon error-text"></ui-icon>
        <div class="fail-details">
          <span class="label-large">${description}</span>
          <span class="error-text body-large">${error.message}</span>
        </div>
      </div>
    `
  }
}
