import type { Data, Fields, Info, RowHook, TableHook, Meta, Errors } from "../../../types"
import { v4 } from "uuid"
import { ErrorSources, ErrorType } from "../../../types"
import { normalizeCountryCode } from "./countryUtils"


type DataWithMeta<T extends string> = Data<T> & Meta & {
  __index?: string;
}

export const addErrorsAndRunHooks = async <T extends string>(
  data: (Data<T> & Partial<Meta>)[],
  fields: Fields<T>,
  rowHook?: RowHook<T>,
  tableHook?: TableHook<T>,
  changedRowIndexes?: number[],
  options?: { costIsTotalCost?: boolean },
): Promise<DataWithMeta<T>[]> => {
  const errors: Errors = {}

  const addError = (source: ErrorSources, rowIndex: number, fieldKey: string, error: Info, type: ErrorType = ErrorType.Custom) => {
    errors[rowIndex] = {
      ...errors[rowIndex],
      [fieldKey]: { ...error, source, type },
    }
  }

  let processedData = [...data] as DataWithMeta<T>[]

  if (tableHook) {
    const tableResults = await tableHook(processedData)
    processedData = tableResults.map((result, index) => ({
      ...processedData[index],
      ...result
    }))
  }

  if (rowHook) {
    if (changedRowIndexes) {
      for (const index of changedRowIndexes) {
        const rowResult = await rowHook(processedData[index], index, processedData)
        processedData[index] = {
          ...processedData[index],
          ...rowResult
        }
      }
    } else {
      const rowResults = await Promise.all(
        processedData.map(async (value, index) => {
          const result = await rowHook(value, index, processedData)
          return {
            ...value,
            ...result
          }
        })
      )
      processedData = rowResults
    }
  }

  // Extract numeric value from qty_per_unit (e.g. "pack of 25" → "25")
  processedData.forEach((row) => {
    const qty = (row as Record<string, unknown>).qty_per_unit
    if (typeof qty === "string" && qty.trim()) {
      const match = qty.match(/\d+/)
      if (match) {
        (row as Record<string, unknown>).qty_per_unit = match[0]
      }
    }
  })

  // Divide cost_each by qty_per_unit when cost represents total cost
  if (options?.costIsTotalCost) {
    processedData.forEach((row) => {
      const r = row as Record<string, unknown>
      const cost = parseFloat(String(r.cost_each ?? ""))
      const qty = parseInt(String(r.qty_per_unit ?? ""), 10)
      if (!isNaN(cost) && qty > 0) {
        r.cost_each = (cost / qty).toFixed(2)
      }
    })
  }

  // Normalize country of origin (coo) to 2-letter ISO codes
  processedData.forEach((row) => {
    const coo = (row as Record<string, unknown>).coo
    if (typeof coo === "string" && coo.trim()) {
      const raw = coo.trim()
      const normalized = normalizeCountryCode(raw)
      if (normalized) {
        (row as Record<string, unknown>).coo = normalized
      } else if (raw.length === 2) {
        // Uppercase 2-letter values as fallback
        (row as Record<string, unknown>).coo = raw.toUpperCase()
      }
    }
  })

  fields.forEach((field) => {
    const fieldKey = field.key as string
    field.validations?.forEach((validation) => {
      switch (validation.rule) {
        case "unique": {
          const values = processedData.map((entry) => {
            const value = entry[fieldKey as keyof typeof entry]
            return value
          })

          const taken = new Set() // Set of items used at least once
          const duplicates = new Set() // Set of items used multiple times

          values.forEach((value) => {
            if (validation.allowEmpty && !value) {
              // If allowEmpty is set, we will not validate falsy fields such as undefined or empty string.
              return
            }

            if (taken.has(value)) {
              duplicates.add(value)
            } else {
              taken.add(value)
            }
          })

          values.forEach((value, index) => {
            if (duplicates.has(value)) {
              addError(ErrorSources.Table, index, fieldKey, {
                level: validation.level || "error",
                message: validation.errorMessage || "Field must be unique",
              }, ErrorType.Unique)
            }
          })
          break
        }
        case "required": {
          const dataToValidate = changedRowIndexes ? changedRowIndexes.map((index) => processedData[index]) : processedData
          dataToValidate.forEach((entry, index) => {
            const realIndex = changedRowIndexes ? changedRowIndexes[index] : index
            const value = entry[fieldKey as keyof typeof entry]
            if (value === null || value === undefined || value === "") {
              addError(ErrorSources.Row, realIndex, fieldKey, {
                level: validation.level || "error",
                message: validation.errorMessage || "Field is required",
              }, ErrorType.Required)
            }
          })
          break
        }
        case "regex": {
          const dataToValidate = changedRowIndexes ? changedRowIndexes.map((index) => processedData[index]) : processedData
          const regex = new RegExp(validation.value, validation.flags)
          dataToValidate.forEach((entry, index) => {
            const realIndex = changedRowIndexes ? changedRowIndexes[index] : index
            const value = entry[fieldKey as keyof typeof entry]
            const stringValue = value?.toString() ?? ""
            if (!stringValue.match(regex)) {
              addError(ErrorSources.Row, realIndex, fieldKey, {
                level: validation.level || "error",
                message:
                  validation.errorMessage || `Field did not match the regex /${validation.value}/${validation.flags} `,
              }, ErrorType.Regex)
            }
          })
          break
        }
      }
    })
  })

  return processedData.map((value) => {
    // This is required only for table. Mutates to prevent needless rerenders
    const result: DataWithMeta<T> = { ...value }
    if (!result.__index) {
      result.__index = v4()
    }

    // We no longer store errors in the row data
    // The errors are now only stored in the validationErrors Map
    return result
  })
} 