import { Injectable } from '@angular/core'
import { BehaviorSubject } from 'rxjs'
import { LicenseKeyData } from '../models/LicenseKeyData'
import { SasService } from './sas.service'
import moment from 'moment'
import * as base64Converter from 'base64-arraybuffer'
import * as encoding from 'text-encoding'
import { Router } from '@angular/router'
import { Globvar } from '../models/sas/public-startupservice.model'
import { freeTierConfig } from '../free-tier.config'
import { AppStoreService } from './app-store.service'
import { HelperService } from './helper.service'
import { LicenceFeaturesMap, LicenceState } from '../models/LicenceState'
import { LoggerService } from './logger.service'
import { EventService } from './event.service'

@Injectable({
  providedIn: 'root'
})
export class LicenceService {
  public userCountLimitation: boolean = false //toggler for user count limit registration

  public licenceKey: string | undefined
  public activationKey: string | undefined
  private licenseKeyData: LicenseKeyData | null = null

  private _licenceState: LicenceState = freeTierConfig
  private allFeaturesOn: LicenceState = {
    viewer_rows_allowed: Infinity,
    editor_rows_allowed: Infinity,
    stage_rows_allowed: Infinity,
    history_rows_allowed: Infinity,
    submit_rows_limit: Infinity,
    tables_in_library_limit: Infinity,
    viewbox_limit: Infinity,
    lineage_daily_limit: Infinity,
    viewbox: true,
    fileUpload: true,
    editRecord: true,
    addRecord: true
  }

  private freeTierLicenceData: LicenseKeyData = {
    demo: true,
    hot_license_key: this.appStoreService.getDcAdapterSettings()?.hotLicenceKey,
    users_allowed: this._licenceState.users_allowed || 1,
    valid_until: moment().add(1, 'year').format('YYYY-MM-DD'),
    site_id: '',
    site_id_multiple: []
  }

  public isAppActivated = new BehaviorSubject<boolean | null>(null)
  public isAppOverCapacity = new BehaviorSubject<boolean>(false)
  public currentUserCanRegister = new BehaviorSubject<boolean>(false)
  public appLocked = new BehaviorSubject<boolean>(false)
  public licenseExpiresInDays = new BehaviorSubject<number | null>(null)
  public isAppFreeTier = new BehaviorSubject<boolean>(false)
  public licenceProblem = new BehaviorSubject<string | null>(null) //contains the link for particular licence error if any
  public hot_license_key = new BehaviorSubject<string | undefined>(undefined)
  public licenceState: BehaviorSubject<LicenceState> =
    new BehaviorSubject<LicenceState>(this._licenceState)

  constructor(
    private loggerService: LoggerService,
    private appStoreService: AppStoreService,
    private eventService: EventService,
    private sasService: SasService,
    private helperService: HelperService,
    private router: Router
  ) {
    this.sasService.incorrectSiteIdEmitter.subscribe((missmatchId: string) => {
      if (this.isAppActivated.value !== null && !this.isAppFreeTier.value) {
        const url = `/licensing/key?error=missmatch&missmatchId=${missmatchId}&force=true`
        this.licenceProblem.next(url)
        this.deactivateApp(url)
      }
    })
  }

  public async activation(startupservice: any) {
    this.freeTierLicenceData.hot_license_key =
      this.appStoreService.getDcAdapterSettings()?.hotLicenceKey

    await this.setStartupserviceRules(startupservice)

    await this.licensing(startupservice.globvars, startupservice.SYSSITE)
  }

  /**
   * Sets feature restrictions included in startupservice
   * @param startupservice SAS service response
   */
  public setStartupserviceRules(startupservice: any) {
    // Whether or not to disable EDIT RECORD modal
    this._licenceState.editRecord = !(
      startupservice.globvars[0].DC_RESTRICT_EDITRECORD === 'YES'
    )
  }

  /**
   * Parsing the licence key, checking if it's valid, expired etc.
   * It also sets the features encoded in the key.
   * @param globvars from response, includes licence key, is user registered and user registered count
   * @param startup_site_id system site id
   */
  public async licensing(globvars: Globvar[], startup_site_id: string) {
    if (!globvars || !globvars[0]) {
      const err = 'Error getting "Globvars" from startupservice response.'
      console.error(err)

      this.isAppActivated.next(false)
      this.router.navigateByUrl(
        `/licensing/key?error=invalid&details=${btoa(err)}`
      )
      return
    }

    let variables = globvars[0]

    if (
      variables.LICENCE_KEY === undefined ||
      variables.ACTIVATION_KEY === undefined ||
      variables.REGISTERCOUNT === null ||
      variables.REGISTERCOUNT === undefined ||
      variables.ISREGISTERED === null ||
      variables.ISREGISTERED === undefined
    ) {
      console.error('Some of globvars are not present')
      this.isAppActivated.next(false)
      this.eventService.showInfoModal(
        'Error',
        'Some of the globvars are not present in the startupservice'
      )
      // this.router.navigateByUrl('/licensing/key?error=missing')
      return
    }

    if (!variables.LICENCE_KEY || !variables.ACTIVATION_KEY) {
      //fall back to free tier if key not present
      // setTimeout(() => {
      return await this.applicationActivation(
        this.freeTierLicenceData,
        variables,
        startup_site_id
      )
      // })

      // return
    }

    this.licenceKey = variables.LICENCE_KEY
    this.activationKey = variables.ACTIVATION_KEY

    await this.decryptLicenseKey(
      variables.LICENCE_KEY,
      variables.ACTIVATION_KEY
    ).then(
      async (decryptedLicenseData: LicenseKeyData) => {
        await this.applicationActivation(
          decryptedLicenseData,
          variables,
          startup_site_id
        )
      },
      async (err: any) => {
        const errString = `Error decrypting license key. ${err}`
        console.error(errString)
        // this.isAppActivated.next(false)
        const url = `/licensing/key?error=invalid&details=${btoa(
          errString
        )}&force=true`
        this.licenceProblem.next(url)

        setTimeout(() => {
          this.applicationActivation(
            this.freeTierLicenceData,
            variables,
            startup_site_id,
            true
          )
          this.router.navigateByUrl(url)
        })
      }
    )
  }

  /**
   * Checks if:
   * - site id match
   * - key is/isn't expired
   * - user can be registered (user limit)
   * @param licenseData
   * @param variables from SAS response
   * @param startup_site_id of current sas environment
   */
  private applicationActivation(
    licenseData: LicenseKeyData,
    variables?: Globvar,
    startup_site_id?: string,
    skipRouting?: boolean
  ) {
    if (!skipRouting) skipRouting = false

    this.setSiteId(licenseData)

    if (!licenseData.demo && variables && startup_site_id) {
      this.handleSiteIdMissmatch(
        licenseData,
        variables,
        startup_site_id,
        skipRouting
      )
    }

    let hotLicenceKey =
      licenseData.hot_license_key !== undefined
        ? licenseData.hot_license_key
        : this.hot_license_key.value

    if (!hotLicenceKey)
      hotLicenceKey = this.appStoreService.getDcAdapterSettings()?.hotLicenceKey

    this.hot_license_key.next(hotLicenceKey)

    if (this.userCountLimitation) {
      this.handleUsersAllowed(licenseData)
    }

    this.licenseKeyData = this.helperService.deepClone(licenseData)
    ;(window as any).appinfo() //Show the table of appinfo in the console

    const { expiry_date, daysToExpiry } = this.calculateExpiry(licenseData)

    ;(window as any).licenseExpiresIn = daysToExpiry
    this.licenseExpiresInDays.next(daysToExpiry)

    if (daysToExpiry <= 0) {
      console.error('License key has expired')

      if (variables && startup_site_id) {
        this.handleExpiry(expiry_date, variables, startup_site_id, skipRouting)
      }

      return
    }

    this.isAppFreeTier.next(!!licenseData.demo)

    if (variables) {
      this.handleUserRegistration(licenseData, variables, skipRouting)
    }

    this.decodeLicenceFeatures(licenseData)
    this.licenceState.next(this._licenceState) //set the global licence state
  }

  /**
   * Decode and set features that are encoded in the key
   * featureValue - used as number/Infinity for limit set
   * featureToggle - used as boolean, based on 1 or 0 encoded in the key
   * @param licenseData from the licence key
   */
  private decodeLicenceFeatures(licenseData: LicenseKeyData) {
    if (!licenseData.features) {
      if (licenseData.demo) {
        return
      } else {
        this._licenceState = this.allFeaturesOn
        return
      }
    }

    const featuresMap = licenseData.features.split(',')
    this._licenceState = {
      ...this._licenceState,
      viewer_rows_allowed: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.viewer_rows_allowed]
      ),
      editor_rows_allowed: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.editor_rows_allowed]
      ),
      stage_rows_allowed: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.stage_rows_allowed]
      ),
      history_rows_allowed: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.history_rows_allowed]
      ),
      submit_rows_limit: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.submit_rows_limit]
      ),
      tables_in_library_limit: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.tables_in_library_limit]
      ),
      viewbox_limit: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.viewbox_limit]
      ),
      lineage_daily_limit: this.parseFeatureValue(
        featuresMap[LicenceFeaturesMap.lineage_daily_limit]
      ),
      viewbox: this.parseFeatureToggle(featuresMap[LicenceFeaturesMap.viewbox]),
      fileUpload: this.parseFeatureToggle(
        featuresMap[LicenceFeaturesMap.fileUpload]
      ),
      editRecord: this.parseFeatureToggle(
        featuresMap[LicenceFeaturesMap.editRecord]
      ),
      addRecord: this.parseFeatureToggle(
        featuresMap[LicenceFeaturesMap.addRecord]
      )
    }

    this.loggerService.log('Licence state:', this._licenceState)
  }

  /**
   * Converts licence key feature code value to the number or Infinity
   * Used for limiting rows, submits etc.
   * @param codeBit from licence key encoded features. Every bit is separated by comma(,) Eg. 5,10,15
   * @returns number
   */
  private parseFeatureValue(codeBit: string): number {
    if (codeBit === '-') return Infinity
    return parseInt(codeBit)
  }

  /**
   * Converts licence key feature code value to the boolean. Depending on if it's 0 or 1
   * @param codeBit from licence key encoded features. Every bit is separated by comma(,) Eg. 1,1,0
   * @returns boolean to turn on or off the value
   */
  private parseFeatureToggle(codeBit: string): boolean {
    return !!parseInt(codeBit)
  }

  /**
   * If decryption fails, key will be marked as invalid and app not activated.
   */
  private decryptLicenseKey(
    licenseKey: string,
    activationKey: string
  ): Promise<LicenseKeyData> {
    return new Promise<LicenseKeyData>(async (resolve, reject) => {
      if (!(window.crypto && window.crypto.subtle)) {
        try {
          let licenseKeyDecoded = atob(licenseKey.split('').reverse().join(''))

          resolve(JSON.parse(licenseKeyDecoded))
        } catch (ex) {
          reject('Error parsing http license key data. ' + ex)
        }
      }

      let licenseKeyBytes = await this.base64ToArrayBuffer(licenseKey).catch(
        (err: any) => {
          reject(err)
        }
      )

      let activationKeyBytes: ArrayBuffer
      const activationKeyBytesTemp = await this.base64ToArrayBuffer(
        activationKey
      ).catch((err: any) => {
        reject(err)
      })

      if (!activationKeyBytesTemp) {
        reject('Missing activation key')
        return
      }

      activationKeyBytes = activationKeyBytesTemp

      let decryptionKey

      try {
        decryptionKey = await window.crypto.subtle.importKey(
          'pkcs8',
          activationKeyBytes,
          {
            name: 'RSA-OAEP',
            hash: 'SHA-256'
          },
          true,
          ['decrypt']
        )
      } catch (ex) {
        reject('Unable to import decryption key: ' + ex)
      }

      if (!decryptionKey) {
        reject('Unable to import decryption key')
        return
      }

      if (!licenseKeyBytes) {
        reject('License key bytes missing')
      } else {
        try {
          window.crypto.subtle
            .decrypt(
              {
                name: 'RSA-OAEP',
                hash: { name: 'SHA-256' }
              } as any,
              decryptionKey,
              licenseKeyBytes
            )
            .then(
              (value: any) => {
                let keyDataString = new encoding.TextDecoder().decode(value)

                try {
                  resolve(JSON.parse(keyDataString))
                } catch (ex) {
                  reject('Error parsing license key data. ' + ex)
                }
              },
              (err: any) => {
                reject(err)
              }
            )
        } catch (ex) {
          reject(ex)
        }
      }
    })
  }

  public getHotLicenseKey() {
    return this.hot_license_key.value
  }

  public getLicenseKeyData() {
    return this.licenseKeyData
  }

  public deactivateApp(redirectTo?: string) {
    // this.isAppActivated.next(false)

    this._licenceState = freeTierConfig
    this.applicationActivation(this.freeTierLicenceData)

    if (redirectTo) this.router.navigateByUrl(redirectTo)
  }

  /**
   * Checks fo lineage daily renderings limit
   *
   * @param checkOnly if true it will only check if limit reached
   * othervise it will increment number of renderings.
   * @returns true if limited and false if not limited
   */
  public checkLineageLimit(checkOnly?: boolean): boolean {
    if (this.licenceState.value.lineage_daily_limit === Infinity) return false

    const renders = localStorage.getItem('lineage_renders')
    const todayTimestamp = moment().valueOf()

    if (!renders) {
      if (!checkOnly) this.setLineageRender(todayTimestamp, 1)
      return false
    }

    const timestamp = parseInt(renders.split(',')[0])
    const noOfRenders = parseInt(renders.split(',')[1])

    if (moment(timestamp).isSame(moment(), 'day')) {
      if (noOfRenders >= this.licenceState.value.lineage_daily_limit)
        return true

      if (!checkOnly) this.setLineageRender(todayTimestamp, noOfRenders + 1)
      return false
    }

    if (!checkOnly) this.setLineageRender(todayTimestamp, 1)
    return false
  }

  private setLineageRender(timestamp: number, renders: number) {
    localStorage.setItem('lineage_renders', `${timestamp},${renders}`)
  }

  public base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
    return new Promise(async (resolve, reject) => {
      resolve(base64Converter.decode(base64))
    })
  }

  public arrayBufferToBase64(arrayBuffer: ArrayBuffer): Promise<string> {
    return new Promise((resolve, reject) => {
      resolve(base64Converter.encode(arrayBuffer))
    })
  }

  private setSiteId(licenseData: LicenseKeyData) {
    if (licenseData.site_id_multiple) {
      this.sasService.setLicenseSiteId(licenseData.site_id_multiple)
    } else if (licenseData.site_id) {
      this.sasService.setLicenseSiteId(licenseData.site_id)
    }
  }

  /**
   * One of our customers is having multiple site ids in same organization.
   * So we added support for list od site ids to be placed in licence key.
   * But we also want to have dc backwards compatible so we have to check
   * for both properties legacy and new.
   * New key must work with old DC
   * Old key must work with new DC
   */
  private handleSiteIdMissmatch(
    licenseData: LicenseKeyData,
    variables: Globvar,
    startup_site_id: string,
    skipRouting: boolean
  ) {
    let noLegacySiteId: boolean = false
    let noMultipleSiteId: boolean = false

    if (!licenseData.site_id || licenseData.site_id !== startup_site_id) {
      noLegacySiteId = true
    }

    if (
      !licenseData.site_id_multiple ||
      !licenseData.site_id_multiple.includes(startup_site_id)
    ) {
      noMultipleSiteId = true
    }

    if (noLegacySiteId && noMultipleSiteId) {
      console.error('The key provided is for different organization.')
      // this.isAppActivated.next(false)

      setTimeout(() => {
        const url = '/licensing/key?error=missmatch&force=true'
        this.licenceProblem.next(url)

        this._licenceState = freeTierConfig

        this.applicationActivation(
          this.freeTierLicenceData,
          variables,
          startup_site_id,
          true
        )

        if (!skipRouting) this.router.navigateByUrl(url)
      })

      return
    }
  }

  private calculateExpiry(licenseData: LicenseKeyData): {
    expiry_date: moment.Moment
    daysToExpiry: number
  } {
    let expiry_date = moment(licenseData.valid_until, 'YYYY-MM-DD').startOf(
      'day'
    )
    let current_date = moment().startOf('day')
    let daysToExpiry = expiry_date.diff(current_date, 'days')

    return {
      expiry_date,
      daysToExpiry
    }
  }

  private handleExpiry(
    expiry_date: moment.Moment,
    variables: Globvar,
    startup_site_id: string,
    skipRouting: boolean
  ) {
    setTimeout(() => {
      const url = `/licensing/key?force=true&error=expired&details=${btoa(
        'Expiry date: ' + expiry_date.format('DD/MM/YYYY')
      )}`
      this.licenceProblem.next(url)
      // Instead of blocking the app we revert to free tier version of the app
      this.applicationActivation(
        this.freeTierLicenceData,
        variables,
        startup_site_id,
        true
      )

      if (!skipRouting) {
        this.router.navigateByUrl(url)
      }
    })
  }

  private handleUsersAllowed(licenseData: LicenseKeyData) {
    if (licenseData.demo) {
      if (this._licenceState.users_allowed !== undefined)
        licenseData.users_allowed =
          licenseData.users_allowed > 0
            ? licenseData.users_allowed
            : this._licenceState.users_allowed
    }

    this._licenceState.users_allowed = licenseData.users_allowed
  }

  private handleUserRegistration(
    licenseData: LicenseKeyData,
    variables: Globvar,
    skipRouting: boolean
  ) {
    if (this.userCountLimitation) {
      //if limit disabled we don't track the count
      if (variables.REGISTERCOUNT > licenseData.users_allowed) {
        //App is over capacity of users
        console.warn('App has more users registered then licensed.')
        this.isAppOverCapacity.next(true)
      } else if (variables.REGISTERCOUNT === licenseData.users_allowed) {
        //App reached capacity of users
        console.warn('App has hit limit of users registered.')
      }
    }

    if (variables.ISREGISTERED === 1) {
      this.isAppActivated.next(true)
      return
    } else {
      // if limit is disabled we always go into register directly
      if (
        variables.REGISTERCOUNT < licenseData.users_allowed ||
        !this.userCountLimitation
      ) {
        console.log('User can register')
        this.currentUserCanRegister.next(true)
        // this.appLocked.next(true)
        this.isAppActivated.next(false)
        if (!skipRouting) this.router.navigateByUrl('/licensing/register')
        //After register, activate app
        return
      } else {
        this.currentUserCanRegister.next(false)
        this.isAppActivated.next(false)
        // this.appLocked.next(true)
        if (!skipRouting) this.router.navigateByUrl('/licensing/limit')
      }
    }
  }
}
