import { z } from "zod";
import { executionContextSchema } from "../records/context.js";

const LOWER_KEBAB_ID_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
const SEMVER_RANGE_PATTERN = /[\s^~>=<|*]/;
const SEMVER_PATTERN =
  /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;

export function normalizeSemVer(version: string): string {
  if (SEMVER_RANGE_PATTERN.test(version)) {
    throw new Error(`SemVer ranges are forbidden: ${version}`);
  }

  const match = SEMVER_PATTERN.exec(version);
  if (!match) {
    throw new Error(`invalid SemVer: ${version}`);
  }

  const major = Number(match[1]);
  const minor = Number(match[2]);
  const patch = Number(match[3]);
  const prerelease = match[4];
  const build = match[5];

  if (
    !Number.isInteger(major) ||
    !Number.isInteger(minor) ||
    !Number.isInteger(patch) ||
    major < 0 ||
    minor < 0 ||
    patch < 0
  ) {
    throw new Error(`invalid SemVer numeric components: ${version}`);
  }

  let normalized = `${String(major)}.${String(minor)}.${String(patch)}`;
  if (prerelease !== undefined) {
    normalized += `-${prerelease}`;
  }
  if (build !== undefined) {
    normalized += `+${build}`;
  }
  return normalized;
}

function validateLowerKebabId(id: string, path: string): void {
  if (!LOWER_KEBAB_ID_PATTERN.test(id)) {
    throw new Error(`${path} must be a canonical lower-kebab ID`);
  }
}

function validateCanonicalGlob(canonicalGlob: string): void {
  if (canonicalGlob.includes("..")) {
    throw new Error("glob must not contain parent traversal");
  }
  if (canonicalGlob.includes("\\")) {
    throw new Error("glob must not contain backslashes");
  }
  if (canonicalGlob === "*" || canonicalGlob === "**") {
    throw new Error("glob must not use implicit global *");
  }
}

function compareUnicodeScalars(a: string, b: string): number {
  let i = 0;
  let j = 0;

  while (i < a.length && j < b.length) {
    const codeA = a.codePointAt(i);
    const codeB = b.codePointAt(j);
    if (codeA === undefined || codeB === undefined) {
      break;
    }
    if (codeA !== codeB) {
      return codeA < codeB ? -1 : 1;
    }
    i += codeA > 0xffff ? 2 : 1;
    j += codeB > 0xffff ? 2 : 1;
  }

  return a.length - b.length;
}

export const detectorVersionRefSchema = z
  .object({
    id: z.string(),
    version: z.string(),
  })
  .strict()
  .superRefine((value, ctx) => {
    try {
      validateLowerKebabId(value.id, "detector.id");
      value.version = normalizeSemVer(value.version);
    } catch (error) {
      ctx.addIssue({
        code: "custom",
        message: error instanceof Error ? error.message : "invalid detector reference",
      });
    }
  })
  .transform((value) => ({
    id: value.id,
    version: normalizeSemVer(value.version),
  }));

export type DetectorVersionRef = z.infer<typeof detectorVersionRefSchema>;

export const contractVersionRefSchema = z
  .union([
    z
      .object({
        id: z.string(),
        version: z.string(),
      })
      .strict()
      .transform((value) => ({
        id: value.id,
        version: normalizeSemVer(value.version),
      })),
    z.null(),
  ]);

export type ContractVersionRef = z.infer<typeof contractVersionRefSchema>;

export const exactTargetPatternSchema = z
  .object({
    kind: z.literal("exact"),
    canonicalTarget: z.string().min(1),
  })
  .strict();

export const globTargetPatternSchema = z
  .object({
    kind: z.literal("glob"),
    canonicalGlob: z.string().min(1),
  })
  .strict()
  .superRefine((value, ctx) => {
    try {
      validateCanonicalGlob(value.canonicalGlob);
    } catch (error) {
      ctx.addIssue({
        code: "custom",
        message: error instanceof Error ? error.message : "invalid glob",
      });
    }
  });

export const targetPatternSchema = z.discriminatedUnion("kind", [
  exactTargetPatternSchema,
  globTargetPatternSchema,
]);

export type TargetPattern = z.infer<typeof targetPatternSchema>;

export const contextDimensionSchema = z
  .object({
    key: z.string().min(1),
    value: z.string(),
  })
  .strict();

export type ContextDimension = z.infer<typeof contextDimensionSchema>;

export const contextDimensionPredicateSchema = z
  .object({
    key: z.string().min(1),
    operator: z.enum(["equals", "one-of"]),
    values: z.array(z.string()).min(1),
  })
  .strict();

export type ContextDimensionPredicate = z.infer<typeof contextDimensionPredicateSchema>;

const executionContextKindSchema = z.enum([
  "browser",
  "api",
  "cli",
  "worker",
  "stream",
  "filesystem",
  "package",
  "deployment",
]);

export const exactDecisionScopeSchema = z
  .object({
    kind: z.literal("exact"),
    detector: detectorVersionRefSchema,
    target: exactTargetPatternSchema,
    context: z
      .object({
        kind: executionContextKindSchema,
        dimensions: z.array(contextDimensionSchema),
      })
      .strict(),
    contractVersion: contractVersionRefSchema,
  })
  .strict();

export type ExactDecisionScope = z.infer<typeof exactDecisionScopeSchema>;

export const reviewedPolicyScopeSchema = z
  .object({
    kind: z.literal("reviewed-policy"),
    policyId: z.string().min(1),
    detector: detectorVersionRefSchema,
    target: targetPatternSchema,
    context: z
      .object({
        kinds: z.array(executionContextKindSchema).min(1),
        dimensions: z.array(contextDimensionPredicateSchema),
      })
      .strict(),
    contractVersion: contractVersionRefSchema,
    justification: z.string().min(1),
    expiresAt: z.string().min(1),
  })
  .strict();

export type ReviewedPolicyScope = z.infer<typeof reviewedPolicyScopeSchema>;

export const decisionScopeSchema = z.discriminatedUnion("kind", [
  exactDecisionScopeSchema,
  reviewedPolicyScopeSchema,
]);

export type DecisionScope = z.infer<typeof decisionScopeSchema>;

function sortDimensions(dimensions: ContextDimension[]): ContextDimension[] {
  return [...dimensions].sort((left, right) =>
    compareUnicodeScalars(left.key, right.key),
  );
}

function sortPredicates(
  predicates: ContextDimensionPredicate[],
): ContextDimensionPredicate[] {
  return [...predicates]
    .map((predicate) => ({
      ...predicate,
      values: [...predicate.values].sort(compareUnicodeScalars),
    }))
    .sort((left, right) => compareUnicodeScalars(left.key, right.key));
}

function sortKinds(
  kinds: Array<z.infer<typeof executionContextKindSchema>>,
): Array<z.infer<typeof executionContextKindSchema>> {
  return [...kinds].sort(compareUnicodeScalars);
}

export function normalizeDecisionScope(scope: DecisionScope): DecisionScope {
  const parsed = decisionScopeSchema.parse(scope);

  if (parsed.kind === "exact") {
    return {
      ...parsed,
      context: {
        kind: parsed.context.kind,
        dimensions: sortDimensions(parsed.context.dimensions),
      },
    };
  }

  return {
    ...parsed,
    context: {
      kinds: sortKinds(parsed.context.kinds),
      dimensions: sortPredicates(parsed.context.dimensions),
    },
  };
}

export function parseExecutionContextKind(
  value: unknown,
): z.infer<typeof executionContextKindSchema> {
  return executionContextKindSchema.parse(value);
}

export function validateExecutionContext(value: unknown): void {
  executionContextSchema.parse(value);
}
