import { Service } from '../core/Service.js'
import type { IOrganization, UserOrganizationGrantType } from '@api-client/core/models/store/Organization.js'
import { CurrentOrganizationKey } from '../lib/StoreKeys.js'
import services from './api_service.js'

enum State {
  NotChecked,
  Checking,
  Checked,
}

export enum CompletionState {
  /**
   * The user has at least one organization and the current organization is selected.
   */
  OK,
  /**
   * The user has at leas two organizations and no valid selection.
   */
  NotSelected,
  /**
   * The user have no organizations. Setup is needed.
   */
  NoOrganizations,
  /**
   * Unable to determine the state due to an error.
   */
  Error,
}

const OrgsCacheKey = 'user_organizations'

export default class OrganizationService extends Service {
  static override key = 'api-service/organization'

  state: State = State.NotChecked
  organizations: IOrganization[] = []
  organizationId: string | undefined
  /**
   * A pending organization ID that was set via the URL query parameter `oid`.
   * When it is set, it preselects the organization when loading list of organizations.
   * This will override the `organizationId` set in the local storage.
   */
  pendingOid: string | undefined | null

  /**
   * A hidden value for the `updateComplete` getter.
   */
  #updateComplete?: Promise<CompletionState>

  /**
   * The resolver to call when the update completes.
   */
  #updateResolver?: (state: CompletionState) => void

  protected override async onCreate(): Promise<void> {
    const url = new URL(window.location.href)
    this.pendingOid = url.searchParams.get('oid')
    this.revalidate()
  }

  /**
   * @deprecated Use #setOrganization instead
   */
  setupUserOrganization(org: IOrganization): Promise<void> {
    return this.setOrganization(org)
  }

  /**
   * Sets an org as the current user org.
   * @param org The organization to add.
   */
  async setOrganization(org: IOrganization): Promise<void> {
    const hasOrg = this.organizations.some((i) => i.key === org.key)
    if (!hasOrg) {
      this.organizations.push(org)
    }
    const config = await services.get('config')
    await config.local.set(CurrentOrganizationKey, org.key)
    this.organizationId = org.key
  }

  /**
   * Adds an organization to the list of organizations.
   * It also updated the cache. Used when creating a new org.
   * @param org The organization to add.
   */
  async addOrganization(org: IOrganization): Promise<void> {
    const hasOrg = this.organizations.some((i) => i.key === org.key)
    if (!hasOrg) {
      this.organizations.push(org)
      await this.#saveCache()
    }
  }

  /**
   * Removes an organization from the list of organizations.
   * It also updated the cache. Used when deleting an org.
   * @param org The organization to remove.
   */
  async removeOrganization(org: IOrganization): Promise<void> {
    const idx = this.organizations.findIndex((i) => i.key === org.key)
    if (idx !== -1) {
      this.organizations.splice(idx, 1)
      await this.#saveCache()
      if (this.organizationId === org.key) {
        this.organizationId = undefined
        const config = await services.get('config')
        await config.local.delete(CurrentOrganizationKey)
      }
    }
  }

  /**
   * @deprecated Use #readOrganization instead
   */
  getUserOrganization(id: string): Promise<IOrganization> {
    return this.readOrganization(id)
  }

  /**
   * Reads an organization from the list of organizations.
   * @param id The organization ID.
   * @returns The organization.
   */
  async readOrganization(id: string): Promise<IOrganization> {
    await this.#updateComplete
    const org = this.organizations.find((i) => i.key === id)
    if (!org) {
      throw new Error(`Organization with id ${id} not found`)
    }
    return org
  }

  /**
   * Gets the current user organization.
   * @returns The current organization or undefined if not set.
   */
  async currentOrganization(): Promise<IOrganization | undefined> {
    if (this.state === State.Checking) {
      await this.#updateComplete
    }
    if (!this.organizationId) {
      return undefined
    }
    return this.organizations.find((i) => i.key === this.organizationId)
  }

  /**
   * Checks if the user has a specific grant type in the current organization.
   * @param grantType The grant type to check.
   * @returns `true` if the user has the specified grant type, `false` otherwise.
   */
  protected async hasGrantType(grantType: UserOrganizationGrantType): Promise<boolean> {
    const org = await this.currentOrganization()
    if (!org) {
      return false
    }
    return org.grantType === grantType
  }

  /**
   * Checks if the user has any of the specified grant types in the current organization.
   * @param types The grant types to check.
   * @returns `true` if the user has any of the specified grant types, `false` otherwise.
   */
  async hasRoles(types: UserOrganizationGrantType[]): Promise<boolean> {
    const org = await this.currentOrganization()
    if (!org) {
      return false
    }
    return types.includes(org.grantType)
  }

  /**
   * Checks if the user is the owner of the current organization.
   * @returns `true` if the user is the owner of the current organization, `false` otherwise.
   */
  async isOwner(): Promise<boolean> {
    return this.hasGrantType('owner')
  }

  /**
   * Checks if the user is a manager of the current organization.
   * @returns `true` if the user is a manager of the current organization, `false` otherwise.
   */
  async isManager(): Promise<boolean> {
    return this.hasGrantType('manager')
  }

  /**
   * Checks if the user is an editor of the current organization.
   * @returns `true` if the user is a editor of the current organization, `false` otherwise.
   */
  async isEditor(): Promise<boolean> {
    return this.hasGrantType('editor')
  }

  /**
   * Forces to get the list of organizations from the server.
   *
   * Returns one of the following states:
   *
   * - `CompletionState.OK` - The user has at least one organization and the current organization is selected.
   * - `CompletionState.NotSelected` - The user has at leas two organizations and no valid selection.
   * - `CompletionState.NoOrganizations` - The user have no organizations. Setup is needed.
   * - `CompletionState.Error` - Unable to determine the state due to an error.
   *
   * @returns The state of the organization creation.
   */
  revalidate(): Promise<CompletionState> {
    if (this.state === State.Checking) {
      return this.#updateComplete as Promise<CompletionState>
    }
    this.state = State.Checking
    this.#updateComplete = new Promise<CompletionState>((resolver) => {
      this.#updateResolver = resolver
    })
    this.#getState()
    return this.#updateComplete
  }

  /**
   * Reads the list of organizations either from the cache or from the server.
   */
  async #getState(): Promise<void> {
    const loaded = await this.#loadCache()
    if (!loaded) {
      try {
        await this.#callOrganizations()
      } catch {
        this.#resolve(CompletionState.Error)
        this.state = State.Checked
        return
      }
    }
    const result = await this.#readState()
    this.state = State.Checked
    this.#resolve(result)
  }

  async #loadCache(): Promise<boolean> {
    const cache = await services.get('cache')
    const orgs = await cache.get<IOrganization[]>(OrgsCacheKey)
    if (orgs) {
      this.organizations = orgs
      return true
    }
    return false
  }

  async #saveCache(): Promise<void> {
    const cache = await services.get('cache')
    if (this.organizations.length === 0) {
      await cache.delete(OrgsCacheKey)
    } else {
      await cache.set<IOrganization[]>(OrgsCacheKey, this.organizations, cache.TTL.OneHour)
    }
  }

  async #callOrganizations(): Promise<void> {
    const sdk = await services.get('sdk')
    const orgs = await sdk.organizations.list()
    this.organizations = orgs.items || []
    await this.#saveCache()
  }

  #resolve(state: CompletionState): void {
    if (this.#updateResolver) {
      this.#updateResolver(state)
      this.#updateResolver = undefined
    }
  }

  async #readState(): Promise<CompletionState> {
    const config = await services.get('config')
    const selected = this.pendingOid ?? ((await config.local.get(CurrentOrganizationKey)) as string | undefined)
    this.pendingOid = undefined // Clear the pending OID after reading it
    const hasSelected = this.organizations.some((i) => i.key === selected)
    let result: CompletionState
    if (selected && hasSelected) {
      this.organizationId = selected
      result = CompletionState.OK
      await config.local.set(CurrentOrganizationKey, selected)
    } else if (this.organizations.length === 1) {
      const key = this.organizations[0].key
      await config.local.set(CurrentOrganizationKey, key)
      this.organizationId = key
      result = CompletionState.OK
    } else if (this.organizations.length) {
      result = CompletionState.NotSelected
    } else {
      result = CompletionState.NoOrganizations
    }
    return result
  }
}
