import type {
  ContextDimension,
  ContextDimensionPredicate,
  ContractVersionRef,
  DecisionScope,
  DetectorVersionRef,
  ExactDecisionScope,
  ReviewedPolicyScope,
  TargetPattern,
} from "../../../schema/src/decisions/scope.js";

export type MatchSubject = {
  detector: DetectorVersionRef;
  target: string;
  contextKind: string;
  dimensions: Record<string, string>;
  contractVersion: ContractVersionRef;
  now: string;
};

export type MatchResult =
  | { matched: true }
  | {
      matched: false;
      reason: "detector" | "target" | "context" | "contract" | "expired";
    };

function detectorsEqual(
  left: DetectorVersionRef,
  right: DetectorVersionRef,
): boolean {
  return left.id === right.id && left.version === right.version;
}

function contractVersionsEqual(
  left: ContractVersionRef,
  right: ContractVersionRef,
): boolean {
  if (left === null && right === null) {
    return true;
  }
  if (left === null || right === null) {
    return false;
  }
  return left.id === right.id && left.version === right.version;
}

function targetSegmentsForGlob(target: string): string[] {
  return target.split("/").filter((segment) => segment.length > 0);
}

function matchSegmentPattern(pattern: string, segment: string): boolean {
  if (!pattern.includes("*")) {
    return pattern === segment;
  }

  const parts = pattern.split("*");
  if (parts.length === 2) {
    const [prefix, suffix] = parts as [string, string];
    return segment.startsWith(prefix) && segment.endsWith(suffix);
  }

  if (parts.length === 1) {
    return segment === pattern;
  }

  const [first, ...rest] = parts;
  const last = rest[rest.length - 1] ?? "";
  const middle = rest.slice(0, -1);

  if (!segment.startsWith(first ?? "")) {
    return false;
  }
  if (!segment.endsWith(last)) {
    return false;
  }

  let remainder = segment.slice((first ?? "").length, segment.length - (last.length));
  for (const middlePart of middle) {
    const index = remainder.indexOf(middlePart);
    if (index === -1) {
      return false;
    }
    remainder = remainder.slice(index + middlePart.length);
  }

  return true;
}

function matchGlobSegments(patternParts: string[], targetSegments: string[]): boolean {
  return matchGlobSegmentsAt(patternParts, targetSegments, 0, 0);
}

function matchGlobSegmentsAt(
  patternParts: string[],
  targetSegments: string[],
  patternIndex: number,
  targetIndex: number,
): boolean {
  if (patternIndex === patternParts.length) {
    return targetIndex === targetSegments.length;
  }

  const patternPart = patternParts[patternIndex];
  if (patternPart === undefined) {
    return false;
  }

  if (patternPart === "**") {
    if (patternIndex === patternParts.length - 1) {
      return true;
    }

    for (let skip = 0; skip <= targetSegments.length - targetIndex; skip += 1) {
      if (
        matchGlobSegmentsAt(
          patternParts,
          targetSegments,
          patternIndex + 1,
          targetIndex + skip,
        )
      ) {
        return true;
      }
    }
    return false;
  }

  const targetPart = targetSegments[targetIndex];
  if (targetPart === undefined) {
    return false;
  }

  if (!matchSegmentPattern(patternPart, targetPart)) {
    return false;
  }

  return matchGlobSegmentsAt(
    patternParts,
    targetSegments,
    patternIndex + 1,
    targetIndex + 1,
  );
}

export function matchCanonicalGlob(canonicalGlob: string, target: string): boolean {
  const patternParts = canonicalGlob.split("/").filter((part) => part.length > 0);
  const targetSegments = targetSegmentsForGlob(target);
  return matchGlobSegments(patternParts, targetSegments);
}

function matchTarget(pattern: TargetPattern, target: string): boolean {
  if (pattern.kind === "exact") {
    return pattern.canonicalTarget === target;
  }

  return matchCanonicalGlob(pattern.canonicalGlob, target);
}

function dimensionsRecord(dimensions: ContextDimension[]): Record<string, string> {
  const record: Record<string, string> = {};
  for (const dimension of dimensions) {
    record[dimension.key] = dimension.value;
  }
  return record;
}

function predicateMatches(
  predicate: ContextDimensionPredicate,
  value: string | undefined,
): boolean {
  if (value === undefined) {
    return false;
  }

  if (predicate.operator === "equals") {
    return predicate.values.length === 1 && predicate.values[0] === value;
  }

  return predicate.values.includes(value);
}

function exactContextMatches(
  scope: ExactDecisionScope["context"],
  subject: MatchSubject,
): boolean {
  if (scope.kind !== subject.contextKind) {
    return false;
  }

  for (const dimension of scope.dimensions) {
    if (subject.dimensions[dimension.key] !== dimension.value) {
      return false;
    }
  }

  return true;
}

function policyContextMatches(
  scope: ReviewedPolicyScope["context"],
  subject: MatchSubject,
): boolean {
  if (!scope.kinds.includes(subject.contextKind as ReviewedPolicyScope["context"]["kinds"][number])) {
    return false;
  }

  for (const predicate of scope.dimensions) {
    if (!predicateMatches(predicate, subject.dimensions[predicate.key])) {
      return false;
    }
  }

  return true;
}

function isPolicyExpired(scope: ReviewedPolicyScope, now: string): boolean {
  return now > scope.expiresAt;
}

export function matchScope(scope: DecisionScope, subject: MatchSubject): MatchResult {
  if (scope.kind === "reviewed-policy" && isPolicyExpired(scope, subject.now)) {
    return { matched: false, reason: "expired" };
  }

  if (!detectorsEqual(scope.detector, subject.detector)) {
    return { matched: false, reason: "detector" };
  }

  if (!contractVersionsEqual(scope.contractVersion, subject.contractVersion)) {
    return { matched: false, reason: "contract" };
  }

  if (!matchTarget(scope.target, subject.target)) {
    return { matched: false, reason: "target" };
  }

  const contextMatches =
    scope.kind === "exact"
      ? exactContextMatches(scope.context, subject)
      : policyContextMatches(scope.context, subject);

  if (!contextMatches) {
    return { matched: false, reason: "context" };
  }

  return { matched: true };
}

function targetsOverlap(left: TargetPattern, right: TargetPattern): boolean {
  if (left.kind === "exact" && right.kind === "exact") {
    return left.canonicalTarget === right.canonicalTarget;
  }

  if (left.kind === "exact" && right.kind === "glob") {
    return matchCanonicalGlob(right.canonicalGlob, left.canonicalTarget);
  }

  if (left.kind === "glob" && right.kind === "exact") {
    return matchCanonicalGlob(left.canonicalGlob, right.canonicalTarget);
  }

  if (left.kind === "glob" && right.kind === "glob") {
    return globPatternsOverlap(left.canonicalGlob, right.canonicalGlob);
  }

  return false;
}

function globPatternsOverlap(left: string, right: string): boolean {
  const leftParts = left.split("/").filter((part) => part.length > 0);
  const rightParts = right.split("/").filter((part) => part.length > 0);

  const leftLiterals = leftParts.filter((part) => part !== "**" && !part.includes("*"));
  const rightLiterals = rightParts.filter((part) => part !== "**" && !part.includes("*"));

  for (const literal of leftLiterals) {
    if (!rightLiterals.includes(literal) && !right.includes("**") && !right.includes("*")) {
      return false;
    }
  }

  if (leftLiterals.length > 0 && rightLiterals.length > 0) {
    const shared = leftLiterals.filter((literal) => rightLiterals.includes(literal));
    if (shared.length === 0 && !left.includes("**") && !right.includes("**")) {
      const leftFirst = leftParts[0];
      const rightFirst = rightParts[0];
      if (
        leftFirst !== undefined &&
        rightFirst !== undefined &&
        leftFirst !== "**" &&
        rightFirst !== "**" &&
        !leftFirst.includes("*") &&
        !rightFirst.includes("*") &&
        leftFirst !== rightFirst
      ) {
        return false;
      }
    }
  }

  return (
    matchCanonicalGlob(left, right) ||
    matchCanonicalGlob(right, left) ||
    wildcardPatternsCompatible(leftParts, rightParts)
  );
}

function wildcardPatternsCompatible(
  leftParts: string[],
  rightParts: string[],
): boolean {
  const maxLength = Math.max(leftParts.length, rightParts.length);

  for (let index = 0; index < maxLength; index += 1) {
    const leftPart = leftParts[index];
    const rightPart = rightParts[index];

    if (leftPart === "**" || rightPart === "**") {
      continue;
    }

    if (leftPart === undefined || rightPart === undefined) {
      if (leftParts.includes("**") || rightParts.includes("**")) {
        continue;
      }
      return false;
    }

    if (!segmentPatternsCompatible(leftPart, rightPart)) {
      return false;
    }
  }

  return true;
}

function segmentPatternsCompatible(left: string, right: string): boolean {
  if (left === right) {
    return true;
  }

  if (!left.includes("*") && !right.includes("*")) {
    return false;
  }

  const candidates = [
    left.replace(/\*/g, "x"),
    right.replace(/\*/g, "x"),
    left.replace(/\*/g, ""),
    right.replace(/\*/g, ""),
  ];

  return candidates.some(
    (candidate) =>
      matchSegmentPattern(left, candidate) && matchSegmentPattern(right, candidate),
  );
}

function predicatesCompatible(
  left: ContextDimensionPredicate | undefined,
  right: ContextDimensionPredicate | undefined,
): boolean {
  if (left === undefined || right === undefined) {
    return true;
  }

  const leftValues = new Set(left.values);
  const rightValues = new Set(right.values);
  const intersection = [...leftValues].filter((value) => rightValues.has(value));
  return intersection.length > 0;
}

function exactContextWithinPolicy(
  exact: ExactDecisionScope["context"],
  policy: ReviewedPolicyScope["context"],
): boolean {
  if (!policy.kinds.includes(exact.kind)) {
    return false;
  }

  const exactDimensions = dimensionsRecord(exact.dimensions);
  for (const predicate of policy.dimensions) {
    if (!predicateMatches(predicate, exactDimensions[predicate.key])) {
      return false;
    }
  }

  return true;
}

function policyContextsOverlap(
  left: ReviewedPolicyScope["context"],
  right: ReviewedPolicyScope["context"],
): boolean {
  const kindsIntersect = left.kinds.some((kind) => right.kinds.includes(kind));
  if (!kindsIntersect) {
    return false;
  }

  const keys = new Set([
    ...left.dimensions.map((predicate) => predicate.key),
    ...right.dimensions.map((predicate) => predicate.key),
  ]);

  const leftByKey = new Map(left.dimensions.map((predicate) => [predicate.key, predicate]));
  const rightByKey = new Map(right.dimensions.map((predicate) => [predicate.key, predicate]));

  for (const key of keys) {
    if (!predicatesCompatible(leftByKey.get(key), rightByKey.get(key))) {
      return false;
    }
  }

  return true;
}

function contextsOverlap(left: DecisionScope, right: DecisionScope): boolean {
  if (left.kind === "exact" && right.kind === "exact") {
    if (left.context.kind !== right.context.kind) {
      return false;
    }

    const leftDimensions = dimensionsRecord(left.context.dimensions);
    const rightDimensions = dimensionsRecord(right.context.dimensions);
    const keys = new Set([...Object.keys(leftDimensions), ...Object.keys(rightDimensions)]);

    for (const key of keys) {
      const leftValue = leftDimensions[key];
      const rightValue = rightDimensions[key];
      if (leftValue !== undefined && rightValue !== undefined && leftValue !== rightValue) {
        return false;
      }
    }

    return true;
  }

  if (left.kind === "exact" && right.kind === "reviewed-policy") {
    return exactContextWithinPolicy(left.context, right.context);
  }

  if (left.kind === "reviewed-policy" && right.kind === "exact") {
    return exactContextWithinPolicy(right.context, left.context);
  }

  if (left.kind === "reviewed-policy" && right.kind === "reviewed-policy") {
    return policyContextsOverlap(left.context, right.context);
  }

  return false;
}

export function scopesOverlap(left: DecisionScope, right: DecisionScope): boolean {
  if (!detectorsEqual(left.detector, right.detector)) {
    return false;
  }

  if (!contractVersionsEqual(left.contractVersion, right.contractVersion)) {
    return false;
  }

  if (!targetsOverlap(left.target, right.target)) {
    return false;
  }

  return contextsOverlap(left, right);
}
