import { isEmptyObject, isEqual, isNil, isPlainObject } from "es-toolkit";

import type { CheckFailure } from "../types";
import type { ExpectationResult, RawValueExpectation, ValueExpectation, ValueMatcher } from "./types";

import { mismatchFailure } from "./failure";
import { MATCHER_KEY_SET } from "./keys";

export function applyValueMatcher(
  actual: unknown,
  matcher: ValueMatcher,
  options: { stringifyNonString?: boolean } = {},
): boolean {
  for (const [key, expected] of Object.entries(matcher)) {
    if (expected === undefined) continue;

    switch (key) {
      case "contains":
        if (!stringValue(actual, options).includes(expected as string)) return false;
        break;
      case "empty": {
        const empty = isEmptyValue(actual);
        if (expected !== empty) return false;
        break;
      }
      case "equals":
        if (!isEqual(actual, expected)) return false;
        break;
      case "exists":
        if (expected) {
          if (actual === undefined) return false;
        } else {
          if (actual !== undefined) return false;
        }
        break;
      case "gt":
        if (!compareNumber(actual, expected as number, (left, right) => left > right)) return false;
        break;
      case "gte":
        if (!compareNumber(actual, expected as number, (left, right) => left >= right)) return false;
        break;
      case "lt":
        if (!compareNumber(actual, expected as number, (left, right) => left < right)) return false;
        break;
      case "lte":
        if (!compareNumber(actual, expected as number, (left, right) => left <= right)) return false;
        break;
      case "regex":
        if (!new RegExp(expected as string).test(stringValue(actual, options))) return false;
        break;
    }
  }

  return true;
}

export function checkValueExpectation(
  actual: unknown,
  expectation: undefined | ValueExpectation,
  options: { message?: string; path: string; phase: CheckFailure["phase"]; stringifyNonString?: boolean },
): ExpectationResult {
  if (expectation === undefined) return { failure: null, matched: true };
  if (applyValueMatcher(actual, expectation, { stringifyNonString: options.stringifyNonString })) {
    return { failure: null, matched: true };
  }
  return {
    failure: mismatchFailure(
      options.phase,
      options.path,
      displayValueExpectation(expectation),
      actual,
      options.message ?? `${options.path} mismatch`,
    ),
    matched: false,
  };
}

export function displayValueExpectation(expectation: ValueExpectation): unknown {
  const entries = Object.entries(expectation).filter(([, value]) => value !== undefined);
  if (entries.length === 1 && entries[0]?.[0] === "equals") return entries[0][1];
  return expectation;
}

export function evaluateJsonPath(json: unknown, path: string): unknown {
  if (!path.startsWith("$.")) return undefined;

  const segments = path.slice(2).split(".");
  let current: unknown = json;

  for (const seg of segments) {
    const bracketMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
    if (bracketMatch) {
      if (current === null || current === undefined) return undefined;
      current = (current as Record<string, unknown>)[bracketMatch[1]!];
      const idx = parseInt(bracketMatch[2]!, 10);
      if (!Array.isArray(current) || idx >= current.length) return undefined;
      current = current[idx];
    } else {
      if (current === null || current === undefined) return undefined;
      current = (current as Record<string, unknown>)[seg];
    }
  }

  return current;
}

export function isValueMatcherObject(value: unknown): value is ValueMatcher {
  return isPlainObject(value) && Object.keys(value).some((key) => MATCHER_KEY_SET.has(key));
}

export function isValueMatcherPrimitive(value: unknown): value is boolean | null | number | string {
  return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}

export function resolveValueExpectation(raw: RawValueExpectation): ValueExpectation;
export function resolveValueExpectation(raw: RawValueExpectation | undefined): undefined | ValueExpectation;
export function resolveValueExpectation(raw: RawValueExpectation | undefined): undefined | ValueExpectation {
  if (raw === undefined) return undefined;
  if (isValueMatcherObject(raw)) return raw;
  return { equals: raw };
}

function compareNumber(
  actual: unknown,
  expected: number,
  compare: (actual: number, expected: number) => boolean,
): boolean {
  const value = Number(actual);
  return Number.isFinite(value) && compare(value, expected);
}

function isEmptyValue(value: unknown): boolean {
  return isNil(value) || value === "" || (Array.isArray(value) && value.length === 0) || isEmptyObject(value);
}

function stringValue(actual: unknown, options: { stringifyNonString?: boolean }): string {
  if (!options.stringifyNonString || typeof actual === "string") return String(actual);
  if (actual !== null && typeof actual === "object") return JSON.stringify(actual);
  return String(actual);
}
