import { Injectable } from '@angular/core'
import { HelperService } from './helper.service'
import { VaMessage } from './va-messaging.service'
import { QueryClause } from '../models/TableData'

/** SAS data-type kind used to format a VA filter value into a DC RAW_VALUE. */
type VaColumnKind = 'char' | 'numeric' | 'time' | 'date' | 'datetime'

/**
 * Translates SAS Visual Analytics data-driven content `parameters` into DC
 * filter clauses. Independent of editor/HOT state — the editor passes the
 * loaded column metadata in — so it is unit-testable on its own. Temporal
 * values are converted to SAS internal numbers (matching DC's native filters)
 * via the shared HelperService.
 */
@Injectable({ providedIn: 'root' })
export class VaFilterService {
  private static readonly SAS_MONTHS = [
    'JAN',
    'FEB',
    'MAR',
    'APR',
    'MAY',
    'JUN',
    'JUL',
    'AUG',
    'SEP',
    'OCT',
    'NOV',
    'DEC'
  ]

  constructor(private helperService: HelperService) {}

  /** label/name (lower-cased) -> DC variable (prop) name, skipping the delete col. */
  buildLabelToVarMap(
    cellValidation: any[],
    columnHeader: string[]
  ): Map<string, string> {
    const map = new Map<string, string>()
    for (let i = 1; i < cellValidation.length; i++) {
      const varName = cellValidation[i]?.data
      if (!varName) continue
      map.set(
        (columnHeader[i] ?? varName).toString().trim().toLowerCase(),
        varName
      )
      map.set(varName.toString().trim().toLowerCase(), varName)
    }
    return map
  }

  /**
   * Translates VA `parameters` into DC filter clauses. Each parameter is matched
   * to a DC column by label; a single value becomes `col = value`, multiple
   * values become `col IN (...)`. Values are formatted for the column's SAS kind.
   */
  buildClauses(
    msg: VaMessage,
    labelToVar: Map<string, string>,
    cols: any[]
  ): QueryClause[] {
    const clauses: QueryClause[] = []
    for (const param of msg.parameters || []) {
      if (!param || param.value === undefined || param.value === null) continue
      const key = (param.label ?? param.name ?? '')
        .toString()
        .trim()
        .toLowerCase()
      // A VA param whose label isn't a DC column (beyond case, which is handled)
      // simply doesn't map — skip it.
      const varName = labelToVar.get(key)
      if (!varName) continue

      const kind = this.columnKind(varName, cols)
      const rawValues = Array.isArray(param.value) ? param.value : [param.value]
      // Format/validate every value for the column's SAS type. Numeric columns
      // must receive a finite number (untrusted postMessage input is
      // interpolated into a SAS WHERE clause, so a non-numeric value here would
      // be an injection vector); temporal -> SAS internal number; char ->
      // escaped. Invalid values are dropped.
      const formatted = rawValues
        .filter((v) => v !== undefined && v !== null && v !== '')
        .map((v) => this.formatValueByKind(v, kind))
        .filter((v): v is string => v !== null)
      if (formatted.length === 0) continue
      clauses.push(this.clause(varName, formatted))
    }
    return clauses
  }

  /**
   * Pre-getdata variant used on the deferred initial load, when DC column
   * metadata isn't available. Uses the VA `label` as the column name and infers
   * the SAS kind from the value shape, so the clause matches what the metadata
   * build later produces (no reconcile reload).
   */
  buildInitialClauses(msg: VaMessage): QueryClause[] {
    const clauses: QueryClause[] = []
    for (const param of msg.parameters || []) {
      if (!param || param.value === undefined || param.value === null) continue
      // The DC column name is the VA `label`; `name` is VA's internal id (pr###).
      const varName = (param.label ?? param.name ?? '').toString().trim()
      if (!varName) continue

      const rawValues = Array.isArray(param.value) ? param.value : [param.value]
      const nonEmpty = rawValues.filter(
        (v) => v !== undefined && v !== null && v !== ''
      )
      if (nonEmpty.length === 0) continue

      const formatted = nonEmpty
        .map((v) =>
          this.formatValueByKind(
            v,
            this.inferKindFromValue(String(v).trim(), param.dataType)
          )
        )
        .filter((v): v is string => v !== null)
      if (formatted.length === 0) continue
      clauses.push(this.clause(varName, formatted))
    }
    return clauses
  }

  /**
   * Stable signature of filter clauses for cross-reload dedup. VARIABLE_NM is
   * upper-cased because the metadata-less deferred build uses VA's label case
   * while the metadata build uses DC's stored column case — SAS WHERE clauses
   * are case-insensitive, so those are the same filter and must not trigger a
   * reconcile reload. A genuine value difference still changes the signature.
   */
  signature(clauses: QueryClause[]): string {
    return JSON.stringify(
      clauses.map((c) => ({
        ...c,
        VARIABLE_NM: (c.VARIABLE_NM ?? '').toString().toUpperCase()
      }))
    )
  }

  private clause(varName: string, formatted: string[]): QueryClause {
    return {
      GROUP_LOGIC: 'AND',
      SUBGROUP_LOGIC: 'AND',
      SUBGROUP_ID: 0,
      VARIABLE_NM: varName,
      OPERATOR_NM: formatted.length === 1 ? '=' : 'IN',
      RAW_VALUE:
        formatted.length === 1 ? formatted[0] : `(${formatted.join(',')})`
    }
  }

  /**
   * SAS data-type kind of a column, from the loaded column specs. DDTYPE carries
   * TIME/DATE/DATETIME; checked DATETIME-before-DATE (substring).
   */
  private columnKind(varName: string, cols: any[]): VaColumnKind {
    const col = (cols || []).find(
      (x: any) =>
        (x?.NAME ?? '').toString().toUpperCase() === varName.toUpperCase()
    )
    const ddtype = (col?.DDTYPE ?? '').toString().toUpperCase()
    if (ddtype.includes('DATETIME')) return 'datetime'
    if (ddtype.includes('DATE')) return 'date'
    if (ddtype.includes('TIME')) return 'time'
    if ((col?.TYPE ?? '') === 'num') return 'numeric'
    return 'char'
  }

  /**
   * Infers a column's SAS kind from a VA value string (deferred load, no
   * metadata). VA's temporal strings are distinctive — `ddMMMyyyy[:HH:MM:SS]`
   * and `H:MM:SS` — so they're detected by shape; otherwise fall back to the VA
   * `dataType`.
   */
  private inferKindFromValue(value: string, dataType?: string): VaColumnKind {
    if (/^\d{1,2}[A-Za-z]{3}\d{4}:\d{2}:\d{2}(:\d{2})?$/.test(value)) {
      return 'datetime'
    }
    if (/^\d{1,2}[A-Za-z]{3}\d{4}$/.test(value)) return 'date'
    if (/^\d{1,2}:\d{2}(:\d{2})?(\.\d+)?$/.test(value)) return 'time'
    if (dataType === 'number') return 'numeric'
    return 'char'
  }

  /**
   * Formats one VA filter value into a DC RAW_VALUE for the column kind, or null
   * when invalid (dropped). Matches DC's native filters, which send the NUMERIC
   * INTERNAL value (not SAS literals — those error out in the backend):
   *  - numeric  -> finite number, raw (else null — injection guard)
   *  - time     -> seconds since midnight   (VA: "0:00:02" -> 2)
   *  - date     -> days since 1960-01-01    (VA: "01JUL1960" -> 182)
   *  - datetime -> seconds since 1960-01-01 (VA: "01JAN1960:00:06:05" -> 365)
   *  - char     -> single-quote escaped
   * Temporal strings are regex-parsed (digits/letters/colons only), so a value
   * either yields a finite number or is dropped — also the injection guard.
   * Date/datetime reuse HelperService.convertJsDateToSasDate so the value is
   * byte-identical to what DC's own pickers produce.
   */
  private formatValueByKind(value: any, kind: VaColumnKind): string | null {
    const s = String(value).trim()
    if (s === '') return null
    switch (kind) {
      case 'numeric': {
        const num = Number(s)
        return Number.isFinite(num) ? `${num}` : null
      }
      case 'time': {
        const secs = this.timeToSeconds(s)
        return secs === null ? null : `${secs}`
      }
      case 'date': {
        const d = this.sasDateToJsDate(s)
        return d
          ? `${this.helperService.convertJsDateToSasDate(d, 'days')}`
          : null
      }
      case 'datetime': {
        const d = this.sasDatetimeToJsDate(s)
        return d
          ? `${this.helperService.convertJsDateToSasDate(d, 'seconds')}`
          : null
      }
      default:
        return `'${s.replace(/'/g, "''")}'`
    }
  }

  /** "H:MM[:SS][.f]" -> seconds since midnight, or null if not a valid time. */
  private timeToSeconds(value: string): number | null {
    if (!/^\d{1,2}:\d{2}(:\d{2})?(\.\d+)?$/.test(value)) return null
    const p = value.split('.')[0].split(':')
    const h = Number(p[0])
    const m = Number(p[1])
    const sec = p.length === 3 ? Number(p[2]) : 0
    if (![h, m, sec].every(Number.isFinite)) return null
    return h * 3600 + m * 60 + sec
  }

  /** SAS "ddMMMyyyy" (e.g. 01JUL1960) -> local JS Date, or null. */
  private sasDateToJsDate(value: string): Date | null {
    const m = /^(\d{1,2})([A-Za-z]{3})(\d{4})$/.exec(value)
    if (!m) return null
    const monIdx = VaFilterService.SAS_MONTHS.indexOf(m[2].toUpperCase())
    if (monIdx < 0) return null
    return new Date(Number(m[3]), monIdx, Number(m[1]))
  }

  /** SAS "ddMMMyyyy:HH:MM[:SS]" (e.g. 01JAN1960:00:06:05) -> local JS Date, or null. */
  private sasDatetimeToJsDate(value: string): Date | null {
    const m =
      /^(\d{1,2})([A-Za-z]{3})(\d{4}):(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value)
    if (!m) return null
    const monIdx = VaFilterService.SAS_MONTHS.indexOf(m[2].toUpperCase())
    if (monIdx < 0) return null
    return new Date(
      Number(m[3]),
      monIdx,
      Number(m[1]),
      Number(m[4]),
      Number(m[5]),
      Number(m[6] || 0)
    )
  }
}
