import { Session, Context, CsrfToken, SessionVariable } from './types'
import { asyncForEach, isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/RequestClient'

const MAX_SESSION_COUNT = 1
const RETRY_LIMIT: number = 3
let RETRY_COUNT: number = 0
const INTERNAL_SAS_ERROR = {
  status: 304,
  message: 'Not Modified'
}

export class SessionManager {
  constructor(
    private serverUrl: string,
    private contextName: string,
    private requestClient: RequestClient
  ) {
    if (serverUrl) isUrl(serverUrl)
  }

  private sessions: Session[] = []
  private currentContext: Context | null = null
  private _debug: boolean = false
  private printedSessionState = {
    printed: false,
    state: ''
  }

  public get debug() {
    return this._debug
  }

  public set debug(value: boolean) {
    this._debug = value
  }

  async getSession(accessToken?: string) {
    await this.createSessions(accessToken)
    await this.createAndWaitForSession(accessToken)
    const session = this.sessions.pop()
    const secondsSinceSessionCreation =
      (new Date().getTime() - new Date(session!.creationTimeStamp).getTime()) /
      1000

    if (
      !session!.attributes ||
      secondsSinceSessionCreation >= session!.attributes.sessionInactiveTimeout
    ) {
      await this.createSessions(accessToken)
      const freshSession = this.sessions.pop()

      return freshSession
    }

    return session
  }

  async clearSession(id: string, accessToken?: string) {
    return await this.requestClient
      .delete<Session>(`/compute/sessions/${id}`, accessToken)
      .then(() => {
        this.sessions = this.sessions.filter((s) => s.id !== id)
      })
      .catch((err) => {
        throw prefixMessage(err, 'Error while deleting session. ')
      })
  }

  private async createSessions(accessToken?: string) {
    if (!this.sessions.length) {
      if (!this.currentContext) {
        await this.setCurrentContext(accessToken).catch((err) => {
          throw err
        })
      }

      await asyncForEach(new Array(MAX_SESSION_COUNT), async () => {
        const createdSession = await this.createAndWaitForSession(
          accessToken
        ).catch((err) => {
          throw err
        })

        this.sessions.push(createdSession)
      }).catch((err) => {
        throw err
      })
    }
  }

  private async createAndWaitForSession(accessToken?: string) {
    const { result: createdSession, etag } = await this.requestClient
      .post<Session>(
        `${this.serverUrl}/compute/contexts/${
          this.currentContext!.id
        }/sessions`,
        {},
        accessToken
      )
      .catch((err) => {
        throw err
      })

    await this.waitForSession(createdSession, etag, accessToken)

    this.sessions.push(createdSession)

    return createdSession
  }

  private async setCurrentContext(accessToken?: string) {
    if (!this.currentContext) {
      const { result: contexts } = await this.requestClient
        .get<{
          items: Context[]
        }>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
        .catch((err) => {
          throw err
        })

      const contextsList =
        contexts && contexts.items && contexts.items.length
          ? contexts.items
          : []

      const currentContext = contextsList.find(
        (c: any) => c.name === this.contextName
      )

      if (!currentContext) {
        throw new Error(
          `The context '${this.contextName}' was not found on the server ${this.serverUrl}.`
        )
      }

      this.currentContext = currentContext

      Promise.resolve()
    }
  }

  private getHeaders(accessToken?: string) {
    const headers: any = {
      'Content-Type': 'application/json'
    }

    if (accessToken) {
      headers.Authorization = `Bearer ${accessToken}`
    }

    return headers
  }

  private async waitForSession(
    session: Session,
    etag: string | null,
    accessToken?: string
  ) {
    let sessionState = session.state

    const stateLink = session.links.find((l: any) => l.rel === 'state')

    return new Promise(async (resolve, _) => {
      if (
        sessionState === 'pending' ||
        sessionState === 'running' ||
        sessionState === ''
      ) {
        if (stateLink) {
          if (this.debug && !this.printedSessionState.printed) {
            console.log('Polling session status...')

            this.printedSessionState.printed = true
          }

          const state = await this.getSessionState(
            `${this.serverUrl}${stateLink.href}?wait=30`,
            etag!,
            accessToken
          ).catch((err) => {
            throw err
          })

          sessionState = state.trim()

          if (this.debug && this.printedSessionState.state !== sessionState) {
            console.log(`Current session state is '${sessionState}'`)

            this.printedSessionState.state = sessionState
            this.printedSessionState.printed = false
          }

          // There is an internal error present in SAS Viya 3.5
          // Retry to wait for a session status in such case of SAS internal error
          if (
            sessionState === INTERNAL_SAS_ERROR.message &&
            RETRY_COUNT < RETRY_LIMIT
          ) {
            RETRY_COUNT++

            resolve(this.waitForSession(session, etag, accessToken))
          }

          resolve(sessionState)
        }
      } else {
        resolve(sessionState)
      }
    })
  }

  private async getSessionState(
    url: string,
    etag: string,
    accessToken?: string
  ) {
    return await this.requestClient
      .get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
      .then((res) => res.result as string)
      .catch((err) => {
        if (err.status === INTERNAL_SAS_ERROR.status)
          return INTERNAL_SAS_ERROR.message

        throw err
      })
  }

  async getVariable(sessionId: string, variable: string, accessToken?: string) {
    return await this.requestClient
      .get<SessionVariable>(
        `${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
        accessToken
      )
      .catch((err) => {
        throw prefixMessage(
          err,
          `Error while fetching session variable '${variable}'.`
        )
      })
  }
}
