import { Injectable } from '@angular/core'
import { Subject } from 'rxjs'

import { SpecObj } from '../interfaces'
import { SasService } from './sas.service'
import { HelperService } from './helper.service'

import { QueryClause } from '../models/TableData'
import { globals } from '../_globals'
import { FilterClause, FilterGroup, FilterQuery } from '../models/FilterQuery'
import {
  $DataFormats,
  EditorsGetDataSASResponse,
  EditorsGetDataServiceResponse
} from '../models/sas/editors-getdata.model'
import { LoggerService } from './logger.service'
import { isSpecialMissing } from '@sasjs/utils/input/validators'
import { Col } from '../shared/dc-validator/models/col.model'
import { get } from 'lodash-es'
import { EditorsStageDataSASResponse } from '../models/sas/editors-stagedata.model'
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'

@Injectable()
export class SasStoreService {
  public response: Subject<any> = new Subject<any>()
  public changedTable: Subject<any> = new Subject<any>()
  public details: Subject<any> = new Subject<any>()
  public diffs: Subject<any> = new Subject<any>()
  public columns: Subject<any> = new Subject<any>()
  public values: Subject<any> = new Subject<any>()
  public filter: Subject<any> = new Subject<any>()
  public query: Subject<any> = new Subject<any>()
  public submittDetail: Subject<any> = new Subject<any>()
  public removeQuery: Subject<any> = new Subject<any>()
  public setSubmit: Subject<any> = new Subject<any>()
  public setSubmitList: Subject<any> = new Subject<any>()

  constructor(
    private sasService: SasService,
    private helperService: HelperService,
    private loggerService: LoggerService
  ) {}

  /**
   * Wrapper for making request to service
   * Should be removed, as it's redundant now
   * TODO: Refactor to call editors/getdata directly
   * @param tableData
   * @param tableName
   * @param program
   * @param libds
   * @returns
   */
  public async callService(
    tableData: Array<any>,
    tableName: string,
    program: string,
    libds: string
  ) {
    const tables: any = {}
    tables[tableName] = [tableData]
    const res: RequestWrapperResponse<EditorsGetDataSASResponse> =
      await this.sasService.request(program, tables)
    const response: EditorsGetDataServiceResponse = {
      data: res.adapterResponse,
      libds: libds
    }
    return response
  }

  /**
   * Calling editors/stagedata - saving table data, sending request to backend
   * @param tableParams params to send to backend
   * @param tableData data to be updated
   * @param tableName name of the table to be updated
   * @param program service against which we send request
   * @param $dataFormats column data formats recieved from backend, sending it back
   * @returns adapter.request() response
   */
  public async updateTable(
    tableParams: any,
    tableData: any,
    tableName: string,
    program: string,
    $dataFormats: $DataFormats | null,
    suppressErrorSuccessMessages?: boolean,
    adapterConfig?: any
  ): Promise<RequestWrapperResponse<EditorsStageDataSASResponse>> {
    // add sp as third argument of createData call

    let tables: any = {
      jsdata: tableData
    }

    if ($dataFormats) {
      let formats = this.parseFormats($dataFormats)

      tables.$jsdata = { formats: formats }
    }

    tables[tableName] = [tableParams]

    let res = await this.sasService.request<EditorsStageDataSASResponse>(
      program,
      tables,
      adapterConfig,
      {
        suppressErrorAbortModal: suppressErrorSuccessMessages,
        suppressSuccessAbortModal: suppressErrorSuccessMessages
      }
    )

    return res
  }

  /**
   * Sending request to 'approvers/getapprovals' to fetch approvals list
   * @param tableData sending to backend table data
   * @param tableName sending to backend table name
   * @param program service to run request on
   * @returns HTTP Response
   */
  public async getApprovals(
    tableData: any,
    tableName: string,
    program: string
  ) {
    let tables: any = {}
    tables[tableName] = [tableData]
    return (await this.sasService.request(program, tables)).adapterResponse
  }

  /**
   * Interceptor for loading of the submitted details
   * @param detail submitter
   * @param index submitter index
   * @param data submit data
   */
  public async sendDetails(detail: any, index: any, data: any) {
    let details = Object.assign({ sub: true }, detail)
    let subData = data[index]
    let allData = {
      data: subData,
      viewData: details
    }
    this.submittDetail.next(allData)
  }

  /**
   *
   * @returns All submits
   */
  public async getSubmitts() {
    return (await this.sasService.request('editors/getsubmits', null))
      .adapterResponse
  }

  private libsPromise: Promise<any> | null = null

  /**
   *
   * @returns All libraries
   */
  public viewLibs() {
    if (!this.libsPromise) {
      this.libsPromise = this.sasService
        .request('public/viewlibs', null)
        .then((res: any) => res.adapterResponse)
        .catch((err: any) => {
          this.libsPromise = null
          throw err
        })
    }
    return this.libsPromise
  }

  public async refreshLibInfo(libref: string) {
    const data = {
      lib2refresh: [{ libref }]
    }

    return (await this.sasService.request('public/refreshlibinfo', data))
      .adapterResponse
  }

  public async viewTables(lib: any) {
    let tables = { SASControlTable: [{ MPLIB: lib }] }
    return (await this.sasService.request('public/viewtables', tables))
      .adapterResponse
  }

  public async viewData(libDataset: any, filter_pk: any) {
    let tables = {
      SASControlTable: [{ LIBDS: libDataset, FILTER_RK: filter_pk }]
    }
    return (await this.sasService.request('public/viewdata', tables))
      .adapterResponse
  }

  public async viewDataSearch(
    searchVal: string,
    numeric: boolean = false,
    libDataset: any,
    filter_pk: any
  ) {
    let SEARCHTYPE = numeric ? 'NUM' : 'CHAR'

    let tables = {
      SASControlTable: [
        {
          SEARCHTYPE: searchVal.length > 0 ? SEARCHTYPE : 'NONE',
          SEARCHVAL: searchVal,
          LIBDS: libDataset,
          FILTER_RK: filter_pk
        }
      ]
    }

    return (await this.sasService.request('public/viewdata', tables))
      .adapterResponse
  }

  public async getXLMapRules(id: string) {
    const tables = {
      getxlmaps_in: [{ XLMAP_ID: id }]
    }
    return (await this.sasService.request('editors/getxlmaps', tables))
      .adapterResponse
  }

  public async showDiffs(tableData: any, tableName: string, program: string) {
    let tables: any = {}
    tables[tableName] = [tableData]
    return (
      await this.sasService.request(program, tables, {
        useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable
      })
    ).adapterResponse
  }
  public async rejecting(tableData: any, tableName: string, program: string) {
    let tables: any = {}
    tables[tableName] = [tableData]
    return (
      await this.sasService.request(program, tables, {
        useComputeApi: null // Using WEB APPROACH as a temporary workaround until VIYA JES API is fixed. For other server types then VIYA this is not applicable
      })
    ).adapterResponse
  }

  public async approveTable(
    tableData: any,
    tableName: string,
    program: string
  ) {
    let tables: any = {}
    tables[tableName] = [tableData]

    return (await this.sasService.request(program, tables)).adapterResponse
  }

  public async getHistory(tableData: any, tableName: string, program: string) {
    let tables: any = {}
    tables[tableName] = [tableData]
    return (await this.sasService.request(program, tables)).adapterResponse
  }

  setQueryVariables(dataset: string, cols: any) {
    let columnsData: any = { data: { cols: cols }, libds: dataset }

    this.columns.next(columnsData)
  }

  public async getChangeInfo(tableId: any) {
    let obj = { TABLE: tableId }
    let table = { SASControlTable: [obj] }
    return (await this.sasService.request('public/getchangeinfo', table))
      .adapterResponse
  }

  public async getQueryValues(
    variable: string,
    dataset: string,
    filterQuery: any[],
    STARTROW?: number,
    ROWS?: number
  ) {
    let tables: any = {
      iwant: [
        {
          libds: dataset,
          col: variable,
          ...(STARTROW && { STARTROW: STARTROW }), //add only if present
          ...(ROWS && { ROWS: ROWS }) //add only if present
        }
      ]
    }

    if (filterQuery.length > 0) {
      tables.FILTERQUERY = filterQuery
    }

    return (
      await this.sasService
        .request('public/getcolvals', tables)
        .catch((er: any) => {
          throw er
        })
    ).adapterResponse
  }

  public async saveQuery(libds: string, filterQuery: QueryClause[]) {
    let tables = {
      iwant: [{ filter_table: libds }],
      filterquery: filterQuery
    }

    const res = await this.sasService.request('public/validatefilter', tables)

    this.filter.next(res)

    return res.adapterResponse
  }

  public async openTable(tableId: string) {
    let tables = { iwant: [{ table_id: tableId }] }
    return (await this.sasService.request('auditors/getstagetable', tables))
      .adapterResponse
  }

  public checkOperator(operator: any, value: any, type: string) {
    let val
    let operators = ['=', '>', '<', '<=', '>=', 'ne']
    switch (operator) {
      case 'BETWEEN':
        if (value instanceof Array) {
          if (value[0] !== '' && value[1] !== '') {
            val = value[0] + ' AND ' + value[1]
          } else if (value[0] !== '' && value[1] === '') {
            value[1] = '.'
            val = value[0] + ' AND ' + value[1]
          } else if (value[0] === '' && value[1] !== '') {
            value[0] = '.'
            val = value[0] + ' AND ' + value[1]
          } else {
            value[0] = '.'
            value[1] = '.'
            val = value[0] + ' AND ' + value[1]
          }
        }
        break
      case 'IN':
      case 'NOT IN':
        let arr = []
        if (typeof value !== 'undefined') {
          for (let ind = 0; ind < value.length; ind++) {
            if (value[ind].checked === true) {
              if (type === 'char') {
                if (typeof value[ind].val === 'string') {
                  value[ind].val = value[ind].val.replace("'", "''")
                }

                arr.push("'" + value[ind].val + "'")
              } else {
                if (value[ind].val === null) {
                  value[ind].val = '.'
                }
                arr.push(value[ind].val)
              }
            }
          }
        }
        value = '(' + arr + ')'
        break
      case 'CONTAINS':
        operator = '?'
        break
      case 'NOT EQUAL':
        operator = 'ne'
        break
      default:
        break
    }

    if (type === 'num' && value === null) {
      value = '.'
    }

    if (value === 'Please select value') {
      value = ''
    }

    if (operators.indexOf(operator) !== -1 && type === 'num' && value === '') {
      value = '.'
    }

    if (operator === 'BETWEEN') {
      return { value: val, operator: operator }
    } else {
      return { value: value, operator: operator }
    }
  }

  public whereClauseCreator(
    clauses: any,
    groupOperator: string,
    libds: string
  ) {
    let whereString: string = ''
    let clausesArr = clauses.queryObj
    let clausesLogic = clauses.groupLogic
    let opr
    let where = ''
    let clauseArr = []

    for (let index = 0; index < clauses.queryObj.length; index++) {
      let string = ''
      let clause = clauses.queryObj[index]

      for (let ind = 0; ind < clause.elements.length; ind++) {
        let query = clause.elements[ind]

        if (ind < clause.elements.length - 1) {
          opr = clause.clauseLogic
        } else {
          opr = ''
        }

        let val: any

        for (let k = 0; k < query.values.length; k++) {
          if (
            typeof query.value === 'string' &&
            typeof query.values[k].formatted === 'number'
          ) {
            if (query.value === JSON.stringify(query.values[k].formatted)) {
              val = query.values[k].unformatted
            }
          } else {
            let dropValue =
              typeof query.values[k].formatted !== 'number' &&
              query.values[k].formatted !== null
                ? query.values[k].formatted.trim()
                : query.values[k].formatted

            if (query.value === dropValue) {
              val = query.values[k].unformatted
            }
          }

          let operators = ['=', '>', '<', '<=', '>=', 'ne']

          if (
            query.value === 'Please select value' &&
            operators.indexOf(query.operator) !== -1
          ) {
            val = ''
          }

          if (
            query.operator === 'CONTAINS' ||
            query.operator === 'LIKE' ||
            query.operator === 'BEGINS_WITH'
          ) {
            val = query.value
          }
        }

        if (!val) {
          val = query.value
        }

        if (query.value instanceof Array && query.operator === 'BETWEEN') {
          val = []
          val.push(query.value[0])
          val.push(query.value[1])
          for (let i = 0; i < query.values.length; i++) {
            if (val[0] === query.values[i].formatted) {
              val[0] = query.values[i].unformatted
            }
            if (val[1] === query.values[i].formatted) {
              val[1] = query.values[i].unformatted
            }
          }
        }

        if (
          query.value instanceof Array &&
          (query.operator === 'IN' || query.operator === 'NOT IN')
        ) {
          val = []
          val = query.value
          for (let i = 0; i < query.values.length; i++) {
            if (val[i] && val[i].val === query.values[i].formatted) {
              val[i].val = query.values[i].unformatted
            }
          }
        }

        let type = query.type
        //if the value is variable, omit quotes in the 'where' string
        const isValueVariable = query.valueVariable
        let variable = query.variable === null ? '' : query.variable
        let oper = query.operator === null ? '' : query.operator
        // let value = val === null ? "''" : val;
        let value
        value = this.checkOperator(oper, val, type).value
        if (typeof value === 'string' && value[0] !== '(') {
          value = value.replace("'", "''")
        }
        oper = this.checkOperator(oper, value, type).operator

        if (type === 'char' && oper !== 'IN' && oper !== 'NOT IN') {
          if (typeof value === 'undefined') {
            value = ''
          }

          if (isValueVariable) {
            value = ' ' + value + ' ' //without quotes, with spaces
          } else {
            value = " '" + value + "' " //with quotes and spaces
          }

          string = string + ' ' + variable + ' ' + oper + value + opr
        } else {
          if (type === 'num' && typeof value === 'undefined') {
            value = '.'
          }

          if (typeof value === 'undefined') {
            value = ''
          } else {
            value = ' ' + value + ' '
          }
          string = string + ' ' + variable + ' ' + oper + value + opr
        }
      }
      clauseArr.push(string)
    }
    where = ''
    let groupOper
    for (let i = 0; i < clauseArr.length; i++) {
      if (i < clauseArr.length - 1) {
        groupOper = groupOperator
      } else {
        groupOper = ''
      }
      if (clauseArr.length === 1) {
        where = clauseArr[0]
      } else {
        where = where + ' (' + clauseArr[i] + ') ' + groupOper
      }
    }

    whereString = where.substr(1).slice(0, -1)
    where = 'WHERE' + where + ';'

    let queryClause = {
      whereClause: where,
      string: whereString,
      obj: clauses,
      libds: libds
    }
    this.query.next(queryClause)
    return { whereClause: where, string: whereString }
  }

  /**
   * This function creates Filter Query Table
   * @param filterQuery an object of type FilterQuery
   * @returns {QueryClause[]} array of QueryClause
   */
  public createFilterQueryTable(filterQuery: FilterQuery): QueryClause[] {
    const filterQueryClauseTable: QueryClause[] = []

    filterQuery.filterGroups.forEach(
      (filterGroup: FilterGroup, groupIndex: number) => {
        filterGroup.filterClauses.forEach((filterClause: FilterClause) => {
          let rawValue = ''
          if (filterClause.operator === 'BETWEEN') {
            rawValue = `${filterClause.value[0]} AND ${filterClause.value[1]}`
          } else if (
            filterClause.operator === 'IN' ||
            filterClause.operator === 'NOT IN'
          ) {
            filterClause.value.forEach((element: any) => {
              if (element.checked) {
                const value = element.val
                if (
                  typeof value === 'string' &&
                  (!isSpecialMissing(value) || filterClause.type === 'char')
                ) {
                  if (rawValue.length > 0) {
                    rawValue += `,'${value.replace(/'/g, "''")}'`
                  } else {
                    rawValue = `('${value.replace(/'/g, "''")}'`
                  }
                } else {
                  if (rawValue.length > 0) {
                    rawValue += `,${value}`
                  } else {
                    rawValue = `(${value}`
                  }
                }
              }
            })
            if (rawValue.length > 0) {
              rawValue += ')'
            }
          } else if (filterClause.value === '') {
            if (filterClause.type === 'char') {
              rawValue = `' '`
            } else {
              rawValue = '.'
            }
          } else {
            if (filterClause.type === 'char' && !filterClause.valueVariable) {
              rawValue = `'${filterClause.value.replace(/'/g, "''")}'`
            }
          }
          filterQueryClauseTable.push({
            GROUP_LOGIC: filterQuery.groupLogic,
            SUBGROUP_LOGIC: filterGroup.clauseLogic || 'AND',
            SUBGROUP_ID: groupIndex,
            VARIABLE_NM: filterClause.variable,
            OPERATOR_NM: filterClause.operator,
            RAW_VALUE: rawValue || filterClause.value
          })
        })
      }
    )

    return filterQueryClauseTable
  }

  /**
   * This helper function initializes global filter when user load the filtered url
   * @param path it tells whether function is being called from viewer component or editor
   * @param cols array of columns metadata
   */
  public initializeGlobalFilterClause(page: string, cols: Col[]): void {
    const clauses: any = {
      queryObj: [],
      clauseLogic: [],
      groupLogic: get(globals, page).filter.query[0].GROUP_LOGIC
    }

    get(globals, page).filter.clauses = this.helperService.deepClone(clauses)
    get(globals, page).filter.groupLogic = get(
      globals,
      page
    ).filter.query[0].GROUP_LOGIC

    let subGroupLogic = ''
    let group: any = { elements: [] }
    let groupID = 0

    get(globals, page).filter.query.forEach((obj: any) => {
      const temp: any = {}
      for (let i = 0; i < cols.length; i++) {
        if (cols[i].NAME === obj.VARIABLE_NM) {
          subGroupLogic = obj.SUBGROUP_LOGIC
          temp['ddtype'] = cols[i].DDTYPE
          temp['type'] = cols[i].TYPE

          if (cols[i].TYPE === 'num') {
            temp['operators'] = globals.operators.numOperators
          } else {
            temp['operators'] = globals.operators.charOperators
          }

          temp['logic'] = null
          temp['operator'] = obj.OPERATOR_NM
          temp['variable'] = obj.VARIABLE_NM

          if (cols[i].TYPE === 'char') {
            if (obj.OPERATOR_NM === 'IN' || obj.OPERATOR_NM === 'NOT IN') {
              const newValueArray: { checked: boolean; val: any }[] = []
              const valueString = obj.RAW_VALUE.slice(1, -1)
              const valueArray = valueString.split("','")

              if (valueArray.length === 1) {
                newValueArray.push({
                  checked: true,
                  val: valueArray[0].slice(1, -1).replace(/''/g, "'")
                })
              } else {
                valueArray.forEach((element: string, index: number) => {
                  if (index === 0) {
                    newValueArray.push({
                      checked: true,
                      val: element.slice(1).replace(/''/g, "'")
                    })
                  } else if (index === valueArray.length - 1) {
                    newValueArray.push({
                      checked: true,
                      val: element.slice(0, -1).replace(/''/g, "'")
                    })
                  } else {
                    newValueArray.push({
                      checked: true,
                      val: element.replace(/''/g, "'")
                    })
                  }
                })
              }
              temp['value'] = this.helperService.deepClone(newValueArray)
            } else {
              temp['value'] = obj.RAW_VALUE.slice(1, -1).replace(/''/g, "'")
            }
          } else {
            if (obj.OPERATOR_NM === 'IN' || obj.OPERATOR_NM === 'NOT IN') {
              const newValueArray: { checked: boolean; val: any }[] = []
              const valueString = obj.RAW_VALUE.slice(1, -1)
              const valueArray = valueString.split(',')
              valueArray.forEach((element: string, index: number) => {
                newValueArray.push({
                  checked: true,
                  val: element
                })
              })
              temp['value'] = this.helperService.deepClone(newValueArray)
            } else if (obj.OPERATOR_NM === 'BETWEEN') {
              const valueArray = obj.RAW_VALUE.split(' AND ')
              temp['value'] = valueArray
            } else {
              temp['value'] = obj.RAW_VALUE
            }
          }

          temp['values'] = []
          if (groupID === obj.SUBGROUP_ID) {
            group.elements.push(this.helperService.deepClone(temp))
          } else {
            get(globals, page).filter.clauses.queryObj.push(
              this.helperService.deepClone(group)
            )
            get(globals, page).filter.clauses.queryObj[groupID].clauseLogic =
              subGroupLogic
            get(globals, page).filter.clauses.queryObj[groupID].invalidClause =
              false
            groupID++
            group.elements = []
            group.elements.push(this.helperService.deepClone(temp))
          }
          break
        }
      }
    })
    if (group.elements.length > 0) {
      get(globals, page).filter.clauses.queryObj.push(
        this.helperService.deepClone(group)
      )
      get(globals, page).filter.clauses.queryObj[groupID].clauseLogic =
        subGroupLogic
      get(globals, page).filter.clauses.queryObj[groupID].invalidClause = false
    }
    get(globals, page).filter.query = []
  }

  public async removeClause() {
    let removeFlag = true
    this.removeQuery.next(removeFlag)
    return removeFlag
  }

  public specConversion(spec: any) {
    let specObj: SpecObj = {}
    let namesArr = []
    namesArr = spec.map(function (item: any) {
      return item.NAME.toUpperCase()
    })
    if (spec instanceof Array) {
      for (let index = 0; index < namesArr.length; index++) {
        if (spec[index].TYPE === 1) {
          specObj[namesArr[index]] = {
            colType: 'num',
            colLength: spec[index].LENGTH
          }
        } else {
          specObj[namesArr[index]] = {
            colType: 'string',
            colLength: spec[index].LENGTH
          }
        }
      }
    }
    return specObj
  }

  setSubmitReady() {
    let ready = true
    this.setSubmit.next(ready)
  }

  parseFormats($dataFormats: $DataFormats) {
    const formats: {
      [key: string]: any
    } = {}
    const vars = $dataFormats.vars

    for (let col of Object.keys(vars)) {
      const type = vars[col].type
      const length = vars[col].length

      if (type === 'num') {
        formats[col] = 'best.'
      } else if (type === 'char') {
        formats[col] = `$char${length}.`
      }
    }

    return formats
  }
}
