import type {
  ConditionGroupLogic,
  ConditionLogic,
  ConditionParam,
  Conditional,
} from "@/definitions/shared/conditions"

/**
 * Evaluator for the v2 condition contract.
 *
 * Accepts a `Conditional` (anything with `condition`, `conditionParams`,
 * `features`, or `permissions`) and a data model. Returns true if the
 * element/card should render.
 *
 * Precedence:
 *   1. `condition` function (if provided) — short-circuits everything.
 *   2. `features` / `permissions` — checked against the optional
 *      `availableFeatures` / `availablePermissions` lists in `ctx`.
 *   3. `conditionParams` — every entry must pass (default AND); the
 *      group logic is overridden by `conditionGroupLogic`.
 *
 * `subCondition: true` entries are themselves groups with their own
 * `conditionParams` and `conditionGroupLogic`.
 */
export interface EvaluationContext {
  /** Feature flags currently enabled for the consumer. */
  availableFeatures?: ReadonlyArray<string>
  /** Permissions held by the current user. */
  availablePermissions?: ReadonlyArray<string>
}

export function shouldRender(
  cfg: Conditional | undefined,
  item: Record<string, unknown>,
  ctx: EvaluationContext = {},
): boolean {
  if (!cfg) {
    return true
  }

  if (typeof cfg.condition === "function") {
    return Boolean(cfg.condition(item))
  }

  if (cfg.features?.length) {
    const available = ctx.availableFeatures ?? []
    if (!cfg.features.every((f) => available.includes(f))) {
      return false
    }
  }

  if (cfg.permissions?.length) {
    const available = ctx.availablePermissions ?? []
    if (!cfg.permissions.every((p) => available.includes(p))) {
      return false
    }
  }

  if (!cfg.conditionParams?.length) {
    return true
  }

  return evaluateConditionGroup(
    cfg.conditionParams,
    cfg.conditionGroupLogic ?? "AND",
    item,
  )
}

function evaluateConditionGroup(
  params: ReadonlyArray<ConditionParam>,
  logic: ConditionGroupLogic,
  item: Record<string, unknown>,
): boolean {
  if (params.length === 0) {
    return true
  }
  const results = params.map((p) => evaluateCondition(p, item))
  return logic === "OR" ? results.some(Boolean) : results.every(Boolean)
}

function evaluateCondition(
  param: ConditionParam,
  item: Record<string, unknown>,
): boolean {
  if (param.subCondition) {
    return evaluateConditionGroup(
      param.conditionParams ?? [],
      param.conditionGroupLogic ?? "AND",
      item,
    )
  }
  const left = readPath(item, param.name)
  const right =
    param.valueType === "model"
      ? readPath(item, String(param.value ?? ""))
      : param.value
  return applyLogic(param.logic, left, right)
}

function applyLogic(
  logic: ConditionLogic,
  left: unknown,
  right: unknown,
): boolean {
  switch (logic) {
    case "==":
      // eslint-disable-next-line eqeqeq
      return left == right
    case "!=":
      // eslint-disable-next-line eqeqeq
      return left != right
    case ">":
      return toNumber(left) > toNumber(right)
    case ">=":
      return toNumber(left) >= toNumber(right)
    case "<":
      return toNumber(left) < toNumber(right)
    case "<=":
      return toNumber(left) <= toNumber(right)
    case "in-array":
      return Array.isArray(right) ? right.includes(left as never) : false
    case "not-in-array":
      return Array.isArray(right) ? !right.includes(left as never) : true
    case "contains":
      return String(left ?? "").includes(String(right ?? ""))
    case "not-contains":
      return !String(left ?? "").includes(String(right ?? ""))
    case "starts-with":
      return String(left ?? "").startsWith(String(right ?? ""))
    case "ends-with":
      return String(left ?? "").endsWith(String(right ?? ""))
    case "matches":
      try {
        return new RegExp(String(right ?? "")).test(String(left ?? ""))
      } catch {
        return false
      }
    case "is-odd":
      return toNumber(left) % 2 !== 0
    case "is-even":
      return toNumber(left) % 2 === 0
    case "is-truthy":
      return Boolean(left)
    case "is-falsy":
      return !left
    case "is-null":
      return left === null || left === undefined
    case "is-not-null":
      return left !== null && left !== undefined
    case "g0length":
      return lengthOf(left) > 0
    case "n0length":
      return lengthOf(left) === 0
    case "zero-length":
      return lengthOf(left) === 0
    case "length-eq-value":
      return lengthOf(left) === toNumber(right)
    case "length-gt-value":
      return lengthOf(left) > toNumber(right)
    case "length-lt-value":
      return lengthOf(left) < toNumber(right)
    default:
      return false
  }
}

function readPath(item: Record<string, unknown>, path: string): unknown {
  if (!path) {
    return undefined
  }
  const parts = path.split(".")
  let current: unknown = item
  for (const part of parts) {
    if (current && typeof current === "object" && part in (current as object)) {
      current = (current as Record<string, unknown>)[part]
    } else {
      return undefined
    }
  }
  return current
}

function toNumber(value: unknown): number {
  if (typeof value === "number") {
    return value
  }
  if (typeof value === "string" && value.trim() !== "") {
    const n = Number(value)
    return Number.isFinite(n) ? n : NaN
  }
  return NaN
}

function lengthOf(value: unknown): number {
  if (value == null) {
    return 0
  }
  if (Array.isArray(value) || typeof value === "string") {
    return value.length
  }
  if (typeof value === "object") {
    return Object.keys(value as object).length
  }
  return 0
}

/**
 * Internal exports used by tests and the engine package only.
 */
export const __testing = {
  applyLogic,
  evaluateCondition,
  evaluateConditionGroup,
  lengthOf,
  readPath,
  toNumber,
}
