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')
  }

  async getUserOrganization(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
  }

  async currentOrganization(): Promise<IOrganization | undefined> {
    if (this.state === State.Checking) {
      await this.#updateComplete
    }
    if (!this.organizationId) {
      return undefined
    }
    // TODO: we will store the orgs list in the local/session store
    // so we won't be pinging server with every page reload.
    return this.organizations.find((i) => i.key === this.organizationId)
  }

  protected async hasGrantType(grantType: UserOrganizationGrantType): Promise<boolean> {
    const org = await this.currentOrganization()
    if (!org) {
      return false
    }
    return org.grantType === grantType
  }

  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>
    }
    if (this.organizationId) {
      return Promise.resolve(CompletionState.OK)
    }
    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(orgs: IOrganization[]): Promise<void> {
    const cache = await services.get('cache')
    if (orgs.length === 0) {
      await cache.delete(OrgsCacheKey)
    } else {
      await cache.set<IOrganization[]>(OrgsCacheKey, orgs, 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(orgs.items)
  }

  #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
  }

  /**
   * When the user has no organizations, it set the organization as the only one
   * and persists the organization id as the selected organization.
   * @param org The organization to add.
   */
  async setupUserOrganization(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
  }
}
