import { html, type TemplateResult, nothing } from 'lit'
import {
  type CookieConfiguration,
  type DomainEntity,
  type JwtConfiguration,
  SemanticType,
  type SessionConfiguration,
  type ApiModel,
  type DomainProperty,
  type AssociationTarget,
} from '@api-client/core/browser.js'
import { nanoid } from 'nanoid'
import { live } from 'lit/directives/live.js'
import { repeat } from 'lit/directives/repeat.js'
import { state } from 'lit/decorators.js'
import { ApiModelingScreen } from './ApiModelingScreen.js'
import type { UiSelectChangeEvent, UiSelectElement } from '../../md/select/ui-select.js'
import type { UiMenuItemElement } from '../../md/menu/ui-menu-item.js'
import type { UiSwitchElement } from '../../md/switch/ui-switch.js'
import { renderHelp } from '../../help/render.js'
import { getFieldDataTypeLabel, getFieldIcon } from '../lib/ui_mapping.js'
import type { IconType } from '../../md/icons/Icons.js'
import { UiOutlinedTextFieldElement } from '../../md/text-field/ui-outlined-text-field.js'
import { UiCheckboxElement } from '../../md/checkbox/ui-checkbox.js'

interface MissingDomainProperty {
  missing: true
  key: string
  namespace?: string
}

/**
 * An API Modeling screen for configuring session management.
 * It allows to:
 * - Select the User entity.
 * - Add session properties from the User entities that are available during the request lifecycle.
 * - Generate session tokens and configure their settings.
 * - Select session transport method.
 */
export default class ApiModelingSessionScreen extends ApiModelingScreen {
  /**
   * All available entities to list the user object from.
   */
  @state() accessor entities: DomainEntity[] = []

  /**
   * The data model for the user object.
   */
  @state() accessor userModel: DomainEntity | undefined

  /**
   * The list of session properties that are used to identify the user.
   * This is computed during the data setup and when properties list change.
   */
  @state() accessor sessionProperties: (DomainProperty | MissingDomainProperty)[] = []

  /**
   * The list of user object properties.
   */
  @state() accessor userObjectProperties: DomainProperty[] = []

  protected override handleModelChange(model: ApiModel | undefined): void {
    this.entities = []
    if (!model || !model.domain) {
      return
    }
    let changed = false
    let semanticUserEntity: AssociationTarget | undefined
    for (const entity of model.domain.listEntities()) {
      this.entities.push(entity)
      if (!semanticUserEntity && entity.hasSemantic(SemanticType.User)) {
        semanticUserEntity = { key: entity.key }
      }
    }
    for (const entity of model.domain.listAllForeignEntities()) {
      this.entities.push(entity)
      if (!semanticUserEntity && entity.hasSemantic(SemanticType.User)) {
        semanticUserEntity = { key: entity.key, domain: entity.namespace }
      }
    }
    if (!model.user && semanticUserEntity) {
      model.user = semanticUserEntity
      changed = true
    }
    if (model.user) {
      this.userModel = model.domain.findEntity(model.user.key, model.user.domain)
      const session = this.ensureSession(model)
      if (!session.properties || session.properties.length === 0) {
        session.properties = this.findDefaultSessionProperties(this.userModel)
        changed = true
      }
    }
    this.handleUserObjectChange()
    if (changed) {
      model.notifyChange()
    }
  }

  findDefaultSessionProperties(user?: DomainEntity): string[] {
    const result: string[] = []
    if (!user) {
      return result
    }
    for (const prop of user.properties) {
      if (prop.primary) {
        result.push(prop.key)
      } else if (prop.hasSemantic(SemanticType.UserRole)) {
        result.push(prop.key)
      } else if (prop.hasSemantic(SemanticType.DeletedFlag)) {
        result.push(prop.key)
      } else if (prop.hasSemantic(SemanticType.DeletedTimestamp)) {
        result.push(prop.key)
      }
    }
    return result
  }

  updateUserObjectProperties(): void {
    this.userObjectProperties = []
    if (this.userModel) {
      for (const prop of this.userModel.properties) {
        this.userObjectProperties.push(prop)
      }
    }
  }

  /**
   * Updates the session properties based on the current model.
   * This is called when the user object changes or when the model is updated.
   */
  updateSessionProperties(): void {
    this.sessionProperties = []
    const model = this.model
    if (!model || !model.domain || !this.userModel) {
      return
    }
    if (model.session && model.session.properties) {
      for (const prop of model.session.properties) {
        // We use fields here to save on some computations.
        // The Entity#properties is a generator that look up for a property definition in the domain.
        // We can save on that as we need to check whether the property exists in the user entity
        // and only then we lookup for the full property definition.
        const info = this.userModel.fields.find((f) => f.key === prop)
        const property = info ? model.domain.findProperty(prop, this.userModel.namespace) : undefined
        if (property) {
          this.sessionProperties.push(property)
        } else {
          this.sessionProperties.push({
            key: prop,
            namespace: this.userModel.namespace,
            missing: true,
          } as MissingDomainProperty)
        }
      }
    }
  }

  protected handleUserObjectChange(): void {
    this.updateUserObjectProperties()
    this.updateSessionProperties()
  }

  ensureSession(model: ApiModel): SessionConfiguration {
    if (!model.session) {
      model.session = { properties: [], secret: nanoid(10) }
    }
    return model.session
  }

  ensureCookieConfiguration(session: SessionConfiguration): CookieConfiguration {
    if (!session.cookie) {
      session.cookie = {
        kind: 'cookie',
        name: 'session',
        enabled: true,
        lifetime: '7d',
        httpOnly: true,
        secure: true,
        sameSite: 'none',
      }
    }
    return session.cookie
  }

  ensureJwtConfiguration(session: SessionConfiguration): JwtConfiguration {
    if (!session.jwt) {
      session.jwt = {
        kind: 'jwt',
        enabled: true,
        lifetime: '7d',
      }
    }
    return session.jwt
  }

  protected handleUserSelectionChange(e: CustomEvent<UiSelectChangeEvent>): void {
    const model = this.model
    if (!model || !model.domain) {
      return
    }
    const { item, value } = e.detail
    if (!item) {
      return
    }
    const domain = item.dataset.domain
    if (!value) {
      model.user = undefined
      this.handleUserObjectChange()
      this.requestUpdate()
      return
    }
    const entity = model.domain.findEntity(value, domain)
    if (!entity) {
      throw new Error(`Entity with key ${value} not found in domain ${domain}`)
    }
    // Should it be enforced? The user entity should be marked with User semantic.
    // if (!entity.hasSemantic(SemanticType.User)) {
    //   throw new Error(`Entity with key ${value} is not a User object`)
    // }
    model.user = { key: entity.key, domain: entity.namespace }
    this.userModel = entity
    this.handleUserObjectChange()
    this.requestUpdate()
    this.model?.notifyChange()
  }

  protected handleSessionPropertyMenuSelect(e: CustomEvent<{ item: UiMenuItemElement }>): void {
    const { item } = e.detail
    const value = item.value
    if (!value) {
      return
    }
    this.addSessionProperty(value)
  }

  addSessionProperty(propertyKey: string): void {
    const model = this.model
    if (!model || !model.domain) {
      return
    }
    const session = this.ensureSession(model)
    if (session.properties.includes(propertyKey)) {
      // Property already exists, no need to add it again.
      return
    }
    session.properties.push(propertyKey)
    this.updateSessionProperties()
    this.model?.notifyChange()
  }

  removeSessionProperty(propertyKey: string): void {
    const model = this.model
    if (!model || !model.domain) {
      return
    }
    if (!model.session || !model.session.properties) {
      return
    }
    const index = model.session.properties.indexOf(propertyKey)
    if (index >= 0) {
      model.session.properties.splice(index, 1)
      this.updateSessionProperties()
      this.model?.notifyChange()
    }
  }

  protected handleSessionSecretChange(e: Event): void {
    const input = e.target as UiOutlinedTextFieldElement
    if (!input.checkValidity()) {
      input.reportValidity()
      return
    }
    const model = this.model
    if (!model) {
      return
    }
    if (!model.session) {
      model.session = { properties: [], secret: '' }
    }
    model.session.secret = input.value
    this.model?.notifyChange()
  }

  protected handleGenerateSecret(): void {
    const model = this.model
    if (!model) {
      return
    }
    if (!model.session) {
      model.session = { properties: [], secret: '' }
    }
    model.session.secret = nanoid(10)
    this.model?.notifyChange()
    this.requestUpdate()
  }

  protected handleCookieToggle(e: Event): void {
    const model = this.model
    if (!model) {
      return
    }
    const checkbox = e.target as UiSwitchElement
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    cookie.enabled = !!checkbox.checked
    this.model?.notifyChange()
    this.requestUpdate()
  }

  protected handleJwtToggle(e: Event): void {
    const checkbox = e.target as UiSwitchElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const jwt = this.ensureJwtConfiguration(session)
    jwt.enabled = !!checkbox.checked
    this.model?.notifyChange()
    this.requestUpdate()
  }

  protected handleCookieLifetimeChange(e: Event): void {
    const input = e.target as UiOutlinedTextFieldElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    cookie.lifetime = input.value
    this.model?.notifyChange()
  }

  protected handleCookieNameChange(e: Event): void {
    const input = e.target as UiOutlinedTextFieldElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    cookie.name = input.value
    this.model?.notifyChange()
  }

  protected handleCookieHttpOnlyChange(e: Event): void {
    const checkbox = e.target as UiCheckboxElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    cookie.httpOnly = !!checkbox.checked
    this.model?.notifyChange()
  }

  protected handleCookieSecureChange(e: Event): void {
    const checkbox = e.target as UiCheckboxElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    cookie.secure = !!checkbox.checked
    this.model?.notifyChange()
  }

  protected handleCookieSameSiteChange(e: Event): void {
    const select = e.target as UiSelectElement
    const model = this.model
    if (!model || !select.value) {
      return
    }
    const session = this.ensureSession(model)
    const cookie = this.ensureCookieConfiguration(session)
    const value = select.value.toLowerCase()
    cookie.sameSite = value as 'none' | 'lax'
    this.model?.notifyChange()
  }

  protected handleJwtLifetimeChange(e: Event): void {
    const input = e.target as UiOutlinedTextFieldElement
    const model = this.model
    if (!model) {
      return
    }
    const session = this.ensureSession(model)
    const jwt = this.ensureJwtConfiguration(session)
    jwt.lifetime = input.value
    this.model?.notifyChange()
  }

  override render(): TemplateResult | typeof nothing {
    const model = this.model
    if (!model) {
      return nothing
    }
    return html`
      <div class="screen-container">
        <div class="section-header">
          <h2 class="headline-medium">Session Configuration</h2>
          <span class="body-medium description"
            >Configure session properties, that is, how a user is recognized and stored.</span
          >
        </div>
        ${this.renderUserObjectSection(model)}${this.renderSessionPropertiesSection()}
        <ui-divider type="full"></ui-divider>
        ${this.renderSessionSecuritySection()}
        <ui-divider type="full"></ui-divider>
        ${this.renderSessionTransportSection()}
      </div>
    `
  }

  protected renderUserObjectSection(model: ApiModel): TemplateResult {
    const info = model.user
    const id = info?.key || ''

    return html`
      <div class="config-section">
        <div class="section-header">
          <h3 class="title-large">User object</h3>
          <span class="body-medium description">
            Select the User object which represent an authenticated user in your API. We will use this information to
            build the authentication logic.
          </span>
        </div>
        <div class="row">
          <ui-select
            label="User object"
            name="userObject"
            .value="${live(id)}"
            @change="${this.handleUserSelectionChange}"
          >
            <ui-option value="">
              <div></div>
            </ui-option>
            ${repeat(
              this.entities,
              (entity) => entity.key,
              (entity) => html`
                <ui-option value="${entity.key}" data-domain="${entity.namespace}">
                  <div>${entity.info.getLabel()}</div>
                  <div slot="supporting-text">${entity.info.description}</div>
                </ui-option>
              `
            )}
          </ui-select>
          ${renderHelp('api', 'session:user-object-select')}
        </div>
      </div>
    `
  }

  protected renderSessionPropertiesSection(): TemplateResult {
    const hasModel = !!this.userModel
    return html`
      <div class="config-section">
        <div class="section-header">
          <h3 class="title-large">Session Properties</h3>
          <span class="body-medium description">
            ${this.userModel
              ? `Select the ${this.userModel.info.getLabel()} object properties that will be included in the session for authentication and authorization.`
              : 'Select the User object to configure session properties.'}
          </span>
        </div>
        <div class="row">
          <ui-button ?disabled="${!hasModel}" color="tonal" type="button" popovertarget="add-property-menu">
            <ui-icon role="presentation">add</ui-icon>
            Add property
          </ui-button>
          <ui-menu id="add-property-menu" popover="manual" @select="${this.handleSessionPropertyMenuSelect}">
            ${repeat(
              this.userObjectProperties,
              (prop) => prop.key,
              (prop) => html`
                <ui-menu-item value="${prop.key}" ?disabled="${this.sessionProperties.some((p) => p.key === prop.key)}">
                  <span>${prop.info.getLabel()}</span>
                  <span slot="supporting-text">${prop.info.description}</span>
                </ui-menu-item>
              `
            )}
          </ui-menu>
        </div>
        ${this.renderSessionPropertiesList()}
      </div>
    `
  }

  protected renderSessionPropertiesList(): TemplateResult | typeof nothing {
    const items = this.sessionProperties
    if (!items || items.length === 0) {
      return nothing
    }
    return html`<ui-list class="fields-list"
      >${repeat(
        items,
        (prop) => prop.key,
        (prop) => this.renderProperty(prop)
      )}</ui-list
    >`
  }

  protected renderProperty(item: DomainProperty | MissingDomainProperty): TemplateResult {
    let label: string
    let description: string
    let dataType: string | undefined
    let icon: IconType
    if (this.isMissingProperty(item)) {
      label = 'Missing property'
      description = 'This property is missing in the user object. Please add it to the user object.'
      icon = 'warning'
    } else {
      label = item.info.getLabel()
      description = item.info.description || ''
      dataType = getFieldDataTypeLabel(item)
      icon = getFieldIcon(item)
    }
    return html`<ui-list-item class="property-item" data-key="${item.key}" lines="two">
      <span slot="start"><ui-icon class="icon" icon="${icon}"></ui-icon></span>
      <span>${label}</span>
      <span slot="supporting-text">${description}</span>
      <span slot="end-text">${dataType}</span>
      <ui-icon-button slot="end" @click="${() => this.removeSessionProperty(item.key)}">
        <ui-icon>delete</ui-icon>
      </ui-icon-button>
    </ui-list-item>`
  }

  isMissingProperty(item: DomainProperty | MissingDomainProperty): item is MissingDomainProperty {
    return (item as MissingDomainProperty).missing === true
  }

  protected renderSessionSecuritySection(): TemplateResult {
    const value = this.model?.session?.secret || ''
    return html`
      <div class="config-section">
        <h3 class="title-large">Session Security</h3>
        <span class="body-medium description">
          Configure the session encryption that applies to all data used to exchange user information between the API
          and the client.
        </span>
        <div class="row">
          <ui-outlined-text-field
            label="Encryption secret"
            name="secret"
            .value="${live(value)}"
            @change="${this.handleSessionSecretChange}"
            required
            type="password"
            minLength="8"
            maxLength="36"
            pattern="[a-zA-Z0-9_\\-]*"
          ></ui-outlined-text-field>
          <span class="generate-button">
            <ui-button @click="${this.handleGenerateSecret}">Regenerate</ui-button>
          </span>
        </div>
        <span class="body-medium description">
          The encryption secret is used to encode and decode cookies and JWT. Once changed, all already issued tokens
          and cookies will be invalid.
        </span>
      </div>
    `
  }

  protected renderSessionTransportSection(): TemplateResult {
    return html`
      <div class="config-section">
        <div class="section-header">
          <h3 class="title-large">Session transport</h3>
          <span class="body-medium description"
            >Choose how the session is stored and transported. At least one transport is required.</span
          >
        </div>
        ${this.renderCookieSection()}${this.renderJwtSection()}
      </div>
    `
  }

  protected renderCookieSection(): TemplateResult {
    const enabled = !!this.model?.session?.cookie?.enabled
    return html`
      <div class="session-transport-settings">
        <div class="row checkbox">
          <div class="input enum">
            <ui-switch .checked="${enabled}" id="cookiesEnabled" @change="${this.handleCookieToggle}"></ui-switch>
            <label for="cookiesEnabled" class="body-medium">Cookies</label>
          </div>
          ${renderHelp('api', 'session:cookies-toggle')}
        </div>
        ${this.renderCookieSettings(enabled)}
      </div>
    `
  }

  protected renderCookieSettings(enabled: boolean): TemplateResult | typeof nothing {
    if (!enabled) {
      return nothing
    }
    const cookie = this.model?.session?.cookie
    return html`
      <div class="transport-settings-item separate">
        <span class="body-medium"
          >We created commonly used and recommended settings for you. Feel free to adjust as needed.</span
        >
      </div>
      <div class="transport-settings-item separate">
        <ui-outlined-text-field
          label="Cookie lifetime"
          name="lifetime"
          .value="${live(cookie?.lifetime || '')}"
          @change="${this.handleCookieLifetimeChange}"
          required
          type="text"
          supportingText="The lifetime of the cookie. Use a valid time format, e.g., 7d, 1h, 30m."
        ></ui-outlined-text-field>
      </div>
      <div class="transport-settings-item separate">
        <ui-outlined-text-field
          label="Cookie name"
          name="cookie-name"
          .value="${live(cookie?.name || '')}"
          @change="${this.handleCookieNameChange}"
          required
          type="text"
          pattern="^[a-zA-Z0-9_\\-]+$"
          supportingText="The name of the cookie."
        ></ui-outlined-text-field>
      </div>
      <div class="transport-settings-group">
        <div class="transport-settings-item">
          <div class="row checkbox">
            <div class="input checkbox">
              <ui-checkbox
                id="http-only-flag"
                .checked="${cookie?.httpOnly || false}"
                name="http-only-flag"
                @change="${this.handleCookieHttpOnlyChange}"
              ></ui-checkbox>
              <div class="label">
                <label for="http-only-flag" class="body-medium">HTTP only flag</label>
              </div>
            </div>
            ${renderHelp('api', 'session:http-only-flag')}
          </div>
        </div>
        <div class="transport-settings-item">
          <div class="row checkbox">
            <div class="input checkbox">
              <ui-checkbox
                id="secure-flag"
                .checked="${cookie?.secure || false}"
                name="secure-flag"
                @change="${this.handleCookieSecureChange}"
              ></ui-checkbox>
              <div class="label">
                <label for="secure-flag" class="body-medium">Secure flag</label>
              </div>
            </div>
            ${renderHelp('api', 'session:secure-flag')}
          </div>
        </div>
      </div>
      <div class="transport-settings-item separate">
        <div class="row">
          <ui-select
            label="Cookie same site"
            name="sameSite"
            .value="${live(cookie?.sameSite || '')}"
            @change="${this.handleCookieSameSiteChange}"
          >
            <ui-option value="none">None</ui-option>
            <ui-option value="lax">Lax</ui-option>
          </ui-select>
          ${renderHelp('api', 'session:same-site-cookie-select')}
        </div>
      </div>
    `
  }

  protected renderJwtSection(): TemplateResult {
    const enabled = !!this.model?.session?.jwt?.enabled
    return html`
      <div class="session-transport-settings">
        <div class="row checkbox">
          <div class="input enum">
            <ui-switch .checked="${enabled}" id="jwtEnabled" @change="${this.handleJwtToggle}"></ui-switch>
            <label for="jwtEnabled" class="body-medium">JSON Web Token (JWT)</label>
          </div>
          ${renderHelp('api', 'session:jwt-toggle')}
        </div>
        ${this.renderJwtSettings(enabled)}
      </div>
    `
  }

  protected renderJwtSettings(enabled: boolean): TemplateResult | typeof nothing {
    if (!enabled) {
      return nothing
    }
    const jwt = this.model?.session?.jwt
    return html`
      <div class="transport-settings-item separate">
        <span class="body-medium"
          >We created a commonly used and recommended settings for you. Feel free to adjust as needed.</span
        >
      </div>
      <div class="transport-settings-item separate">
        <ui-outlined-text-field
          label="Token lifetime"
          name="lifetime"
          .value="${live(jwt?.lifetime || '')}"
          @change="${this.handleJwtLifetimeChange}"
          required
          type="text"
          supportingText="The lifetime of the token. Use a valid time format, e.g., 7d, 1h, 30m."
        ></ui-outlined-text-field>
      </div>
    `
  }
}
