import { Injectable, EventEmitter } from '@angular/core'

import SASjs, { UploadFile } from '@sasjs/adapter'
import { BehaviorSubject } from 'rxjs'
import { UserService } from '../shared/user.service'

import { Router } from '@angular/router'
import { EventService } from './event.service'
import { SasjsService } from './sasjs.service'
import { SASjsApiDriveFolderContents } from '../models/sasjs-api/SASjsApiDriveFolderContents.model'
import { ServerType } from '@sasjs/utils/types/serverType'
import { DcAdapterSettings } from '../models/DcAdapterSettings'
import { AppStoreService } from './app-store.service'
import { LoggerService } from './logger.service'
import { RequestWrapperOptions } from '../models/request-wrapper/RequestWrapperOptions'
import { ErrorBody } from '../models/ErrorBody'
import { UploadFileResponse } from '../models/UploadFile'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
import { SasViyaService } from './sas-viya.service'
import { ViyaApiFolder } from '../viya-api-explorer/models/viya-api-folder.model'
import { ViyaApiFolderMembers } from '../viya-api-explorer/models/viya-api-folder-content.model'

@Injectable({
  providedIn: 'root'
})
export class SasService {
  public loadStartupServiceEmitter: EventEmitter<any> = new EventEmitter()
  public incorrectSiteIdEmitter: EventEmitter<string> = new EventEmitter()
  public requestSiteIdEmitter: EventEmitter<string> = new EventEmitter()

  private sasjsAdapter: SASjs = new SASjs()
  private dcAdapterSettings: DcAdapterSettings | undefined
  public serverType: any
  private appLocCheckPending: boolean = false

  public shouldLogin = new BehaviorSubject(false)
  private license_site_id = new BehaviorSubject<string[] | null>(null)

  constructor(
    private appStoreService: AppStoreService,
    private userService: UserService,
    private eventService: EventService,
    private sasjsService: SasjsService,
    private sasViyaService: SasViyaService,
    private loggerService: LoggerService,
    private router: Router
  ) {}

  /**
   * Same as `setup` function in the sasjs.service, this is the constructor replacement.
   * This function is being called by `app.service`.
   * Because of timing and dependency issues
   */
  public sasServiceInit() {
    this.dcAdapterSettings = this.appStoreService.getDcAdapterSettings()

    this.sasjsService.setup()
    this.sasViyaService.setup()

    if (!this.dcAdapterSettings) {
      this.eventService.showInfoModal(
        'Error',
        'Adapter settings (index.html) are not present.'
      )
      return
    }

    this.sasjsAdapter = new SASjs(this.dcAdapterSettings)

    switch (this.dcAdapterSettings.serverType) {
      case ServerType.SasViya: {
        this.checkViyaDeploy(this.dcAdapterSettings.appLoc || '')
        break
      }
      case ServerType.Sas9: {
        this.loadStartupServiceEmitter.emit()
        break
      }
      case ServerType.Sasjs: {
        this.checkSasjsDeploy()
        break
      }
    }

    if (this.getSasjsConfig().loginMechanism === 'Redirected') {
      this.shouldLogin.subscribe((shouldLogin) => {
        if (shouldLogin) {
          this.sasjsAdapter.logIn().then((res) => {
            console.log('res', res)
          })
        }
      })
    }
  }

  /**
   * Runing a backend request against a service.
   * Function also handles the displaying of success or error modals.
   *
   * @param url service to run reuqest against
   * @param data to be sent to backend service
   * @param config additional parameters to force eg. { debug: false }
   * @param wrapperOptions used to provide options to the request wrapper function
   * for example to suppress error or success abort modals after request is finished
   * @returns adapter response or an error. It will return the `log` as well.
   * The log could be potentially be wrong if multiple requests happen because the log this
   * function return is the last request in the Adapter Array for the given URL.
   */
  public request<responseType = any>(
    url: string,
    data: any,
    config?: any,
    wrapperOptions?: RequestWrapperOptions
  ): Promise<RequestWrapperResponse<responseType>> {
    url = 'services/' + url

    if (!wrapperOptions) wrapperOptions = {}

    // If debug is on it will print what is going inside the adapter
    this.loggerService.logRequestData(url, data)

    return new Promise((resolve, reject) => {
      this.sasjsAdapter
        .request(url, data, config, () => {
          this.shouldLogin.next(true)
        })
        .then(
          (res: any) => {
            const sasRequest = this.sasjsAdapter
              .getSasRequests()
              .find((rq) => rq.serviceLink === url)

            if (res.login === false) {
              this.shouldLogin.next(true)
              reject({
                adapterResponse: false,
                log: sasRequest?.logFile
              })
            }

            if (!this.userService.user && res.MF_GETUSER) {
              this.userService.user = {
                username: res.MF_GETUSER
              }
            }

            if (res.SYSSITE) {
              this.requestSiteIdEmitter.emit(res.SYSSITE)

              const licenseSiteId = this.getLicenseSiteId()

              if (licenseSiteId.length > 0) {
                if (!this.getLicenseSiteId().includes(res.SYSSITE)) {
                  this.incorrectSiteIdEmitter.emit(res.SYSSITE)
                }
              }
            }

            if (res.status === 404) {
              reject({
                adapterResponse: {
                  MESSAGE: res.body || 'SAS Responded with error'
                },
                log: sasRequest?.logFile
              })
            }

            if (typeof res.sasjsAbort !== 'undefined') {
              const abortRes = res
              const abortMsg = abortRes.sasjsAbort[0].MSG
              const macMsg = abortRes.sasjsAbort[0].MAC

              if (
                abortMsg.includes(
                  'Data_Controller_Settings(StoredProcess) not found'
                )
              ) {
                this.eventService.startupDataLoaded()
                this.router.navigateByUrl('/deploy')

                reject({
                  adapterResponse: {
                    error: abortMsg
                  },
                  log: sasRequest?.logFile
                })

                return
              }

              if (!wrapperOptions?.suppressSuccessAbortModal) {
                this.eventService.showAbortModal(
                  url.replace('services/', ''),
                  abortMsg,
                  {
                    SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
                    SYSERRORTEXT: abortRes.SYSERRORTEXT,
                    MAC: macMsg
                  }
                )
              }

              reject({
                adapterResponse: {
                  error: abortMsg
                },
                log: sasRequest?.logFile
              })
            }

            resolve({
              adapterResponse: res,
              log: sasRequest?.logFile
            })
          },
          (err: { error: ErrorBody | undefined }) => {
            console.error(err)

            const sasRequest = this.sasjsAdapter
              .getSasRequests()
              .find((rq) => rq.serviceLink === url)

            if (err.error) {
              let errorMessage: string | undefined = err.error.message
              let log: string | undefined

              if (err.error.details && err.error.details.log) {
                log = err.error.details.log
              }

              // If not a single useful info is returned from adapter
              // We display that it's `unknown` SAS service error
              if (!errorMessage || errorMessage.trim().length < 1) {
                errorMessage = 'SAS Service error ocurred'
              }

              // Otherwise we display error message from adapter
              if (!wrapperOptions?.suppressErrorAbortModal) {
                this.eventService.showAbortModal(
                  url,
                  errorMessage,
                  { LOG: log },
                  'Request error'
                )
              }
              reject({
                adapterResponse: {
                  error: errorMessage
                },
                log: sasRequest?.logFile
              })
            }

            reject({
              adapterResponse: err,
              log: sasRequest?.logFile
            })
          }
        )
    })
  }

  /**
   * Uploads a file to the backend, using the adapter upload function.
   *
   * @param sasService Service to which the file will be sent
   * @param files Files to be sent
   * @param params Aditional parameters eg. { debug: false }
   * @returns HTTP Response
   */
  public uploadFile(
    sasService: string,
    files: UploadFile[],
    params: any
  ): Promise<UploadFileResponse> {
    return new Promise((resolve, reject) => {
      this.sasjsAdapter.uploadFile(sasService, files, params).then(
        (res) => {
          const sasRequest = this.sasjsAdapter
            .getSasRequests()
            .find((rq) => rq.serviceLink === 'services/editors/loadfile')

          resolve({
            adapterResponse: res,
            log: sasRequest?.logFile
          })
        },
        (err) => {
          const sasRequest = this.sasjsAdapter
            .getSasRequests()
            .find((rq) => rq.serviceLink === 'services/editors/loadfile')

          reject({
            response: err,
            log: sasRequest?.logFile
          })
        }
      )
    })
  }

  public async login(username: string, password: string) {
    const clientId =
      this.getServerType() === ServerType.Sasjs ? 'clientID1' : undefined

    return this.sasjsAdapter
      .logIn(username, password, clientId)
      .then(
        (res: { isLoggedIn: boolean; userName: string }) => {
          if (res.isLoggedIn) {
            this.userService.user = { username: res.userName }

            if (this.appLocCheckPending) {
              this.checkViyaDeploy(this.dcAdapterSettings?.appLoc || '')
              this.appLocCheckPending = false
            }
          }

          this.shouldLogin.next(!res.isLoggedIn)
          return res.isLoggedIn
        },
        (err: any) => {
          console.error(err)
          this.shouldLogin.next(true)
          return false
        }
      )
      .catch((e: any) => {
        if (e === 403) {
          console.error('Invalid host')
        }
        return false
      })
  }

  public reloadStartupData() {
    this.loadStartupServiceEmitter.emit()
  }

  public getLicenseSiteId(): string[] {
    return this.license_site_id.value || []
  }

  public setLicenseSiteId(value: string | string[]) {
    if (typeof value === 'object') {
      this.license_site_id.next(value)
    } else {
      this.license_site_id.next([value])
    }
  }

  public async checkSasjsDeploy() {
    const sasjsConfig = this.getSasjsConfig()
    const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`

    this.sasjsService.getFolderContentsFromDrive(configuratorFolder).subscribe(
      (contents: SASjsApiDriveFolderContents) => {
        if (contents.files.includes('makedata.sas')) {
          this.eventService.startupDataLoaded()
          this.router.navigateByUrl('/deploy')
        } else {
          this.loadStartupServiceEmitter.emit()

          if (this.router.url.includes('deploy')) this.router.navigateByUrl('/')
        }
      },
      (err: any) => {
        const errorMessage =
          typeof err.error === 'string'
            ? err.error
            : JSON.stringify(err.error || err)

        if (errorMessage.includes('Unauthorized')) {
          this.shouldLogin.next(true)

          this.shouldLogin.subscribe((res: boolean) => {
            if (res === false) location.reload()
          })
        } else if (errorMessage.includes(`Folder doesn't exist.`)) {
          console.warn(
            'SASjs SAS services are not present on the current appLoc.'
          )
          this.eventService.startupDataLoaded()
          this.router.navigateByUrl('/deploy')
        }
      }
    )
  }

  // Required type is NodeJS.Timeout
  // But NodeJS is not available in browser so we have to go with any
  checkingInterval: any
  public async sasjsMakedataChecking(): Promise<boolean> {
    return new Promise(async (resolve, reject) => {
      this.checkingInterval = setInterval(async () => {
        this.sasjsMakedataSuccessfull()
          .then((success: boolean) => {
            if (!!success) {
              clearInterval(this.checkingInterval)
              resolve(success)
            }
          })
          .catch((err: any) => {
            clearInterval(this.checkingInterval)
            reject(err)
          })
      }, 1000)
    })
  }

  private async sasjsMakedataSuccessfull(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const sasjsConfig = this.getSasjsConfig()
      const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`

      this.sasjsService
        .getFolderContentsFromDrive(configuratorFolder)
        .subscribe(
          (contents: SASjsApiDriveFolderContents) => {
            if (!contents.files.includes('makedata.sas')) {
              resolve(true)
            } else {
              resolve(false)
            }
          },
          (err: any) => {
            const errorMessage =
              typeof err.error === 'string'
                ? err.error
                : JSON.stringify(err.error || err)
            if (errorMessage.includes(`Folder doesn't exist.`)) {
              reject()
            }
          }
        )
    })
  }

  public async checkViyaDeploy(path: string) {
    const getFolderExistsInAdapter =
      typeof this.sasjsAdapter.getFolder !== 'undefined'

    let appLocExists: boolean = false
    let errorMessage: string | undefined = undefined

    if (getFolderExistsInAdapter) {
      const results = await this.appLocCheck(path)

      appLocExists = results.found
      errorMessage = results.errorMessage
    } else {
      appLocExists = await this.appLocCheckPreAxiosdAdapter(path)
    }

    if (appLocExists) {
      // Check if there is appLoc/services/admin/makedata.sas present
      // if yes, it needs to be run, so we redirect to /deploy
      // if not, we load the startup service

      this.viyaMakedataSuccessfull().then(
        (success: boolean) => {
          if (success) {
            this.loadStartupServiceEmitter.emit()
          } else {
            this.eventService.startupDataLoaded()
            this.router.navigateByUrl('/deploy')
          }
        },
        (error: any) => {
          console.error('Error while looking for the file: makedata.sas', error)
        }
      )
    } else {
      const errorMessageToShow =
        (errorMessage ||
          'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.') +
        `\nAppLoc: ${path}`

      this.eventService.showInfoModal('Error', errorMessageToShow)
    }
  }

  private async viyaMakedataSuccessfull(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      const sasjsConfig = this.getSasjsConfig()
      const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`

      this.sasViyaService.getFolderByPath(configuratorFolder).subscribe(
        (folderInfo: ViyaApiFolder) => {
          const folderId = folderInfo.id

          if (!folderId) {
            console.error(
              `Folder ID is not present. ${configuratorFolder}`,
              sasjsConfig
            )
            resolve(false)
          }

          this.sasViyaService.getFolderMembers(folderId).subscribe(
            (members: ViyaApiFolderMembers) => {
              if (
                !members.items.some((item: any) => item.name === 'makedata')
              ) {
                // Makedata.sas is not present, which means it was run
                resolve(true)
              } else {
                // Makedata.sas is present, which means it was not run
                resolve(false)
              }
            },
            (err: any) => {
              console.error('Error getting folder contents', err)
              reject()
            }
          )
        },
        (err: any) => {
          console.warn('Error getting folder info', err)
          reject(err)
        }
      )
    })
  }

  public appLocCheck(
    path: string
  ): Promise<{ found: boolean; errorMessage?: string }> {
    return new Promise(async (resolve, reject) => {
      let fetchError: string = ''

      let res: any

      try {
        res = await this.sasjsAdapter.getFolder(path)
      } catch (err: any) {
        if (err.name === 'LoginRequiredError') {
          this.appLocCheckPending = true
          this.shouldLogin.next(true)

          resolve({ found: false })
        } else if (err.name === 'NotFoundeError') {
          fetchError = err.message
        } else {
          fetchError =
            'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.'
        }
      }

      if (fetchError.length) {
        console.warn(fetchError)
        return resolve({ found: false, errorMessage: fetchError })
      }

      resolve({ found: true })
    })
  }

  /**
   * This is a function written before axios adapter where we
   * are getting the folder directly from DC. axios adapter
   * has getFolder() function that provides the folder.
   * @param path The path of the folder of which details we are getting
   */
  public appLocCheckPreAxiosdAdapter(path: string): Promise<boolean> {
    return new Promise((resolve, reject) => {
      let url = `/folders/folders/@item?path=${path}`
      let statusNotFound: boolean = false

      return fetch(url)
        .then((res) => {
          if (res.status === 404) {
            statusNotFound = true
          }

          return res.text()
        })
        .then((res) => {
          if (this.isLoginRequired(res)) {
            this.appLocCheckPending = true
            this.shouldLogin.next(true)
          } else {
            if (statusNotFound) {
              console.warn(
                'Viya services are not present on the current appLoc.'
              )
              this.eventService.startupDataLoaded()
              this.router.navigateByUrl('/deploy')
              return resolve(false)
            }

            let jsonResponse: any = null

            try {
              jsonResponse = JSON.parse(res)
            } catch (ex) {}

            if (jsonResponse) {
              if (jsonResponse.httpStatusCode) {
                if (jsonResponse.httpStatusCode === 404) {
                  console.warn(
                    'Viya services are not present on the current appLoc.'
                  )
                  this.eventService.startupDataLoaded()
                  this.router.navigateByUrl('/deploy')
                  return resolve(false)
                }
              }
            }

            resolve(true)
          }
        })
        .catch((error: any) => {
          resolve(false)
        })
    })
  }

  private isLoginRequired(response: string) {
    const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm
    const matches = pattern.test(response)
    return matches
  }

  public logout() {
    this.sasjsAdapter.logOut().then(() => {
      location.reload()
    })
  }

  public getSasjsConfig() {
    return this.sasjsAdapter.getSasjsConfig()
  }

  public getSasRequests() {
    return this.sasjsAdapter.getSasRequests()
  }

  public setDebugState(state: boolean) {
    this.sasjsAdapter.setDebugState(state)
  }

  /**
   * Returns the `&_debug=...` URL segment honoring the live adapter
   * config. Empty string when debug is off. `128` on the Viya WEB JES path
   * with `runAsTask` enabled, `131` otherwise.
   */
  public getDebugUrlParam(): string {
    const config = this.sasjsAdapter.getSasjsConfig()
    if (!config.debug) return ''
    const value =
      config.serverType === ServerType.SasViya &&
      config.useComputeApi === null &&
      config.runAsTask === true
        ? 128
        : 131
    return `&_debug=${value}`
  }

  public getSasjsInstance() {
    return this.sasjsAdapter
  }

  public getServerType(): string {
    const sasjsConfig = this.sasjsAdapter.getSasjsConfig()

    if (sasjsConfig.serverType) {
      return sasjsConfig.serverType
    }

    return 'SASVIYA'
  }

  public getExecutionPath() {
    const sasjsConfig = this.sasjsAdapter.getSasjsConfig()

    switch (sasjsConfig.serverType) {
      case ServerType.SasViya: {
        return sasjsConfig.pathSASViya
      }
      case ServerType.Sas9: {
        return sasjsConfig.pathSAS9
      }
      case ServerType.Sasjs: {
        return sasjsConfig.pathSASJS
      }
    }
  }

  // Viya specific functions
  public getFileContent(folderPath: string, fileName: string) {
    return this.sasjsAdapter.getFileContent(folderPath, fileName)
  }

  public updateFileContent(
    folderPath: string,
    fileName: string,
    content: string
  ) {
    return this.sasjsAdapter.updateFileContent(folderPath, fileName, content)
  }
}
