import { html, type TemplateResult, type PropertyValues, nothing } from 'lit'
import { repeat } from 'lit/directives/repeat.js'
import {
  type DomainAssociation,
  type MatchResourceOwnerAccessRule,
  type MatchUserRoleAccessRule,
  SemanticType,
  type AccessRule,
  type ApiModel,
  type DomainProperty,
  type DomainEntity,
} from '@api-client/core/browser.js'
import { ApiEditor } from './ApiEditor.js'
import { property, state } from 'lit/decorators.js'
import type { ApiModelAccessTarget, ApiModelOperationName } from '../types.js'
import { isValidAccessRuleType, isValidAccessTarget, isValidOperationName } from '../lib/validators.js'
import { renderHelp } from '../../help/render.js'
import { createEmptyAccessRule } from '../lib/access_helpers.js'

/**
 * An editor for configuring access settings of the API.
 * Access settings can be added either to the API itself, on an endpoint (exposed entity), or to individual operations.
 *
 * This editor doesn't support the dirty state. Any changes made in the editor are immediately reflected in the model.
 */
export default class ApiModelingAccessConfiguration extends ApiEditor {
  /**
   * The operation of the access configuration.
   * Can be one of: `list`, `read`, `create`, `update`, `delete`, or `search`.
   *
   * This property is only relevant when the `target` property is set to `action`.
   * If the `target` property is set to `api` or `endpoint`, this property is ignored.
   *
   * If the value is not set, the editor will not render anything.
   * @attribute
   */
  @property({ type: String }) accessor operation: ApiModelOperationName | undefined

  /**
   * The target of the access configuration.
   * Can be one of: `api`, `action`, or `endpoint`.
   *
   * When the value is set to `api`, the `operation` and `domainKey` properties are ignored.
   * When the value is set to `action`, the `operation` property must be set and the `domainKey` property
   * must point to an exposed entity.
   * When the value is set to `endpoint`, the `operation` property is ignored and the `domainKey` property
   * must point to an exposed entity.
   *
   * If the value is not set, the editor will not render anything.
   * @attribute
   */
  @property({ type: String }) accessor target: ApiModelAccessTarget | undefined

  /**
   * The access rules being edited.
   * This property is set when the `model`, `domainKey`, `operation`, or `target` properties change.
   * The editor should use this property to render the access rules and allow editing them.
   *
   * Note, it is a direct reference to the underlying model. Any changes to this array or its items
   * will be reflected in the model.
   */
  @state() accessor rules: AccessRule[]

  /**
   * The domain property of the user object in the domain model that has the UserRole semantic.
   */
  @state() accessor userRoleProperty: DomainProperty | null = null
  /**
   * An association of the exposed entity that has the ResourceOwner semantic.
   */
  @state() accessor resourceOwnerAssociation: DomainAssociation | null = null

  constructor() {
    super()
    this.operation = undefined
    this.target = undefined
    this.rules = []
  }

  protected override willUpdate(cp: PropertyValues<this>): void {
    if (cp.has('operation') || cp.has('target')) {
      this.setupModel(this.model, this.domainKey)
    } else {
      // We don't need to call `super.willUpdate(cp)` if we are calling `setupModel`
      super.willUpdate(cp)
    }
  }

  protected resetModel(): void {
    this.rules = []
    this.userRoleProperty = null
    this.resourceOwnerAssociation = null
  }

  protected override setupModel(model?: ApiModel, domainKey?: string): void | Promise<void> {
    const { target } = this
    this.resetModel()
    if (!model || !isValidAccessTarget(target)) {
      return
    }
    this.setupRules(model, target, domainKey)
    this.setupUserRoleProperty(model)
    this.setupResourceOwnerAssociation(model, target, domainKey)
  }

  protected setupRules(model: ApiModel, target: ApiModelAccessTarget, domainKey?: string): void {
    const { operation } = this
    if (target === 'api') {
      this.rules = model.accessRule ?? []
      return
    }
    // Everything else requires an `domainKey`
    if (!domainKey) {
      return
    }
    const entity = model.exposes.find((e) => e.key === domainKey)
    if (!entity) {
      return
    }
    if (target === 'endpoint') {
      this.rules = entity.accessRule ?? []
      return
    }
    // Everything else requires an operation
    if (!isValidOperationName(operation)) {
      return
    }
    // At this point we know that target === 'action' and operation is set
    // So we need to find the action in the entity and get its access rules
    const action = entity.actions.find((a) => a.kind === operation)
    this.rules = action?.accessRule ?? []
  }

  protected setupUserRoleProperty(model: ApiModel): void {
    const domain = model.domain
    if (!domain) {
      this.userRoleProperty = null
      return
    }
    // check domain properties first
    for (const entity of domain.listEntities()) {
      if (entity.hasSemantic(SemanticType.User)) {
        this.analyzeUserEntity(entity)
        return
      }
    }
    // check foreign entities next
    for (const entity of domain.listAllForeignEntities()) {
      if (entity.hasSemantic(SemanticType.User)) {
        this.analyzeUserEntity(entity)
        return
      }
    }
  }

  protected analyzeUserEntity(entity: DomainEntity): void {
    let roleProp: DomainProperty | null = null
    for (const property of entity.listProperties()) {
      if (property.hasSemantic(SemanticType.UserRole)) {
        roleProp = property
      }
      if (roleProp) {
        break
      }
    }
    this.userRoleProperty = roleProp
  }

  /**
   * Finds and sets the resource owner association for the exposed entity.
   * @param model The API model
   * @param target The target of the editor
   * @param domainKey The exposed entity id.
   */
  protected setupResourceOwnerAssociation(model: ApiModel, target: ApiModelAccessTarget, domainKey?: string): void {
    if (target === 'api' || !domainKey) {
      return
    }
    const domain = model.domain
    if (!domain) {
      return
    }
    const exposed = model.exposes.find((e) => e.key === domainKey)
    if (!exposed) {
      return
    }
    const entity = domain.findEntity(exposed.entity.key, exposed.entity.domain)
    if (!entity) {
      return
    }
    for (const assoc of entity.listAssociations()) {
      if (assoc.hasSemantic(SemanticType.ResourceOwnerIdentifier)) {
        this.resourceOwnerAssociation = assoc
        return
      }
    }
  }

  protected handleAddModeSelect(e: CustomEvent): void {
    const target = e.detail.item as HTMLElement | undefined
    if (!target) {
      return
    }
    const { item } = target.dataset
    if (!isValidAccessRuleType(item)) {
      return
    }
    this.addEmptyRule(item)
  }

  /**
   * Adds an empty access rule of the given type to the model.
   * @param rule The rule to add
   */
  addEmptyRule(rule: AccessRule['type']): void {
    const src = this.rules
    const info = createEmptyAccessRule(rule)
    if (rule === 'resourceOwner' && this.resourceOwnerAssociation) {
      // Set the property to the resource owner association property
      ;(info as MatchResourceOwnerAccessRule).property = this.resourceOwnerAssociation.key
    }
    src.push(info)
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected synchronizeModel(): void {
    const { operation, target, model } = this
    if (!model || !isValidAccessTarget(target)) {
      return
    }
    const src = this.rules ?? []
    if (target === 'api') {
      model.accessRule = src.length > 0 ? src : undefined
      return
    }
    // Everything else requires an `domainKey`
    const { domainKey: domainKey } = this
    if (!domainKey) {
      return
    }
    const entity = model.exposes.find((e) => e.key === domainKey)
    if (!entity) {
      return
    }
    if (target === 'endpoint') {
      entity.accessRule = src.length > 0 ? src : undefined
      return
    }
    // Everything else requires an operation
    if (!isValidOperationName(operation)) {
      return
    }
    // At this point we know that target === 'action' and operation is set
    // So we need to find the action in the entity and set its access rules
    const action = entity.actions.find((a) => a.kind === operation)
    if (!action) {
      return
    }
    action.accessRule = src.length > 0 ? src : undefined
  }

  protected handleRuleDelete(e: Event): void {
    const button = e.currentTarget as HTMLElement
    const index = Number(button.dataset.index)
    if (Number.isNaN(index) || index < 0 || index >= this.rules.length) {
      return
    }
    this.rules.splice(index, 1)
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected handleAddDomain(e: Event): void {
    const button = e.currentTarget as HTMLElement
    const ruleIndex = Number(button.dataset.ruleIndex)
    if (Number.isNaN(ruleIndex) || ruleIndex < 0 || ruleIndex >= this.rules.length) {
      return
    }
    const rule = this.rules[ruleIndex]
    if (rule.type !== 'matchEmailDomain') {
      return
    }
    if (!Array.isArray(rule.domains)) {
      rule.domains = []
    }
    rule.domains.push('') // Add an empty domain
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected handleDomainDelete(e: Event): void {
    const button = e.currentTarget as HTMLElement
    const index = Number(button.dataset.index)
    const ruleIndex = Number(button.dataset.ruleIndex)
    if (
      Number.isNaN(index) ||
      index < 0 ||
      Number.isNaN(ruleIndex) ||
      ruleIndex < 0 ||
      ruleIndex >= this.rules.length
    ) {
      return
    }
    const rule = this.rules[ruleIndex]
    if (rule.type !== 'matchEmailDomain' || !Array.isArray(rule.domains) || index >= rule.domains.length) {
      return
    }
    rule.domains.splice(index, 1)
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected handleDomainChange(e: Event): void {
    const input = e.currentTarget as HTMLInputElement
    const value = input.value.trim()
    const index = Number(input.dataset.index)
    const ruleIndex = Number(input.dataset.ruleIndex)
    if (
      Number.isNaN(index) ||
      index < 0 ||
      Number.isNaN(ruleIndex) ||
      ruleIndex < 0 ||
      ruleIndex >= this.rules.length
    ) {
      return
    }
    const rule = this.rules[ruleIndex]
    if (rule.type !== 'matchEmailDomain' || !Array.isArray(rule.domains) || index >= rule.domains.length) {
      return
    }
    rule.domains[index] = value
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected handleUserRoleChange(e: Event): void {
    const input = e.currentTarget as HTMLInputElement
    const role = input.value
    const ruleIndex = Number(input.dataset.ruleIndex)
    if (Number.isNaN(ruleIndex) || ruleIndex < 0 || ruleIndex >= this.rules.length) {
      return
    }
    const rule = this.rules[ruleIndex]
    if (rule.type !== 'matchUserRole') {
      return
    }
    if (!Array.isArray(rule.role)) {
      rule.role = []
    }
    if (input.checked) {
      // Add the role
      if (!rule.role.includes(role)) {
        rule.role.push(role)
      }
    } else {
      // Remove the role
      const roleIndex = rule.role.indexOf(role)
      if (roleIndex >= 0) {
        rule.role.splice(roleIndex, 1)
      }
    }
    this.synchronizeModel()
    this.notifyChange()
    this.requestUpdate()
  }

  protected override render(): TemplateResult | typeof nothing {
    if (!isValidAccessTarget(this.target)) {
      return nothing
    }
    return html` ${this.renderIntro()} ${this.renderAddButton()} ${this.renderRules()} `
  }

  protected renderIntro(): TemplateResult {
    return html`<div class="intro">
      <div class="header">
        <span class="title-large">Access mode</span>
        ${renderHelp('modeling', 'api:access-configuration')}
      </div>
      <span class="inline-help body-medium"
        >Add an access mode that controls who can perform an API action on your data. All conditions have to be true to
        allow access.</span
      >
    </div>`
  }

  protected renderAddButton(): TemplateResult {
    const options: TemplateResult[] = [
      html`<ui-list-item role="menuitem" data-item="public">Public</ui-list-item>`,
      html`<ui-list-item role="menuitem" data-item="authenticated">Authenticated</ui-list-item>`,
      html`<ui-list-item role="menuitem" data-item="matchUserRole">User role</ui-list-item>`,
      html`<ui-list-item role="menuitem" data-item="matchEmailDomain">Email match</ui-list-item>`,
    ]
    const { target } = this
    if (target === 'action' || target === 'endpoint') {
      options.push(html`<ui-list-item role="menuitem" data-item="resourceOwner">Resource owner</ui-list-item>`)
    }
    return html`<ui-dropdown-list @select="${this.handleAddModeSelect}" closeOnTab closeOnOutsideClick noOverlap>
      <ui-button color="outlined">
        <ui-icon slot="icon" icon="add" aria-hidden="true"></ui-icon>
        Add mode
      </ui-button>
      <ui-list slot="dropdown" role="menu">${options}</ui-list>
    </ui-dropdown-list>`
  }

  protected renderRules(): TemplateResult | typeof nothing {
    const { rules } = this
    if (rules.length === 0) {
      return nothing
    }
    const result: TemplateResult[] = []
    const len = rules.length
    rules.forEach((rule, index) => {
      result.push(this.renderRule(rule, index))
      if (index < len - 1) {
        result.push(html`<ui-divider class="rule-divider"></ui-divider>`)
      }
    })
    return html`<div class="rules">${result}</div>`
  }

  protected renderRule(rule: AccessRule, index: number): TemplateResult {
    if (rule.type === 'public') {
      return this.renderPublicRule(index)
    }
    if (rule.type === 'authenticated') {
      return this.renderAuthenticatedRule(index)
    }
    if (rule.type === 'resourceOwner') {
      return this.renderResourceOwnerRule(index)
    }
    if (rule.type === 'matchUserRole') {
      return this.renderMatchUserRoleRule(rule, index)
    }
    if (rule.type === 'matchEmailDomain') {
      return this.renderMatchEmailDomainRule(rule, index)
    }
    return html`<div class="rule">
      <span class="rule-title title-medium">Unknown rule</span>
      <span class="rule-description body-medium">This rule type is not supported.</span>
    </div>`
  }

  protected renderRuleHeader(title: string, helpId: string, index: number): TemplateResult {
    return html`<div class="rule-header">
      <span class="rule-title title-medium">${title}</span>
      <div class="actions">
        ${renderHelp('modeling', helpId)}
        <ui-icon-button type="button" data-index="${index}" @click="${this.handleRuleDelete}">
          <ui-icon>delete_outline</ui-icon>
        </ui-icon-button>
      </div>
    </div>`
  }

  protected renderRuleDescription(description: string): TemplateResult {
    return html`<div class="rule-explainer">
      <ui-icon>info</ui-icon>
      <span class="body-medium">${description}</span>
    </div>`
  }

  protected renderPublicRule(index: number): TemplateResult {
    return html`<div class="rule">
      ${this.renderRuleHeader('Public', 'api:access-configuration-public', index)}
      ${this.renderRuleDescription('Everyone can perform this API action.')}
    </div>`
  }

  protected renderAuthenticatedRule(index: number): TemplateResult {
    return html`<div class="rule">
      ${this.renderRuleHeader('Authenticated user', 'api:access-configuration-authenticated', index)}
      ${this.renderRuleDescription('Any authenticated user of the API can perform this API action.')}
    </div>`
  }

  protected renderResourceOwnerRule(index: number): TemplateResult {
    let desc = ''
    if (this.target === 'api') {
      desc = 'This access mode cannot be used on the API level.'
    } else {
      desc = 'Only the resource owner can perform this API action. '
      desc += 'The data entity must define the Resource Owner semantic on one of the properties.'
    }
    return html`<div class="rule">
      ${this.renderRuleHeader('Resource owner', 'api:access-configuration-resource-owner', index)}
      ${this.renderRuleDescription(desc)} ${this.renderResourceOwnerRuleDetails()}
    </div>`
  }

  protected renderResourceOwnerRuleDetails(): TemplateResult {
    let content: TemplateResult
    const { resourceOwnerAssociation } = this
    if (!resourceOwnerAssociation) {
      content = html`<div class="error-message">
        <ui-icon aria-hidden="true">error_outline</ui-icon>
        The owner field is not defined in the domain model.
      </div>`
    } else {
      content = html`<span>Owner field: <strong>${resourceOwnerAssociation.info.getLabel()}</strong></span>`
    }
    return html`<div class="resource-owner-details body-medium">${content}</div>`
  }

  protected renderMatchUserRoleRule(rule: MatchUserRoleAccessRule, index: number): TemplateResult {
    let desc = 'Only users with matched roles can perform this API action. '
    desc += 'The data entity must define the Role semantic on one of the properties.'
    // const roles = rule.role ?? []
    return html`<div class="rule">
      ${this.renderRuleHeader('User role', 'api:access-configuration-match-user-role', index)}
      ${this.renderRuleDescription(desc)} ${this.renderUserRoles(rule, index)}
    </div>`
  }

  protected renderUserRoles(rule: MatchUserRoleAccessRule, ruleIndex: number): TemplateResult {
    const { userRoleProperty } = this
    if (!userRoleProperty) {
      return html`<span class="body-medium"
        >No property found in the domain model that has the UserRole semantic.</span
      >`
    }
    const enums = userRoleProperty.schema?.enum
    if (!Array.isArray(enums) || enums.length === 0) {
      return html`<span class="body-medium">The UserRole property does not define any roles.</span>`
    }
    const roles = rule.role ?? []
    return html`<div class="user-roles">
      <div class="rule-header">
        <span class="rule-title title-small">User roles</span>
        <div class="actions">${renderHelp('modeling', 'api:access-configuration-match-user-role')}</div>
      </div>

      ${repeat(enums, (item: string) => this.renderRoleInput(roles, item, ruleIndex))}
    </div>`
  }

  protected renderRoleInput(roles: string[], role: string, ruleIndex: number): TemplateResult {
    const checked = roles.includes(role)
    return html`<div class="role-input">
      <label class="label-large">
        <ui-checkbox
          ?checked=${checked}
          name="userRole"
          value="${role}"
          data-rule-index="${ruleIndex}"
          @change="${this.handleUserRoleChange}"
        ></ui-checkbox>
        ${role}
      </label>
    </div>`
  }

  protected renderMatchEmailDomainRule(
    rule: Extract<AccessRule, { type: 'matchEmailDomain' }>,
    index: number
  ): TemplateResult {
    const desc = 'Only users with matched email domains can perform this API action.'
    // const domains = rule.domains ?? []
    return html`<div class="rule">
      ${this.renderRuleHeader('Email match', 'api:access-configuration-match-email', index)}
      ${this.renderRuleDescription(desc)} ${this.renderEmailDomains(rule.domains ?? [], index)}
    </div>`
  }

  protected renderEmailDomains(domains: string[], ruleIndex: number): TemplateResult {
    return html`<div class="email-domains">
      <div class="rule-header">
        <span class="rule-title title-small">Email domains</span>
        <div class="actions">${renderHelp('modeling', 'api:access-configuration-match-email-domain')}</div>
      </div>

      ${repeat(domains, (item: string, index: number) => this.renderDomainInput(item, index, ruleIndex))}

      <ui-button color="outlined" data-rule-index="${ruleIndex}" @click="${this.handleAddDomain}">Add domain</ui-button>
    </div>`
  }

  protected renderDomainInput(domain: string, index: number, ruleIndex: number): TemplateResult {
    return html`<div class="domain-input">
      <ui-outlined-text-field
        label="Domain"
        name="domain-${index}"
        .value=${domain}
        @change="${this.handleDomainChange}"
        data-index="${index}"
        data-rule-index="${ruleIndex}"
        class="domain-field"
      ></ui-outlined-text-field>
      <ui-icon-button
        type="button"
        data-index="${index}"
        data-rule-index="${ruleIndex}"
        @click="${this.handleDomainDelete}"
      >
        <ui-icon>delete_outline</ui-icon>
      </ui-icon-button>
    </div>`
  }
}
