import { z } from "zod";
import { truthSourceSchema } from "../graph/evidence-graph.js";

const journeyActorSchema = z
  .object({
    id: z.string(),
    securityBoundary: z.string(),
  })
  .strict();

const journeyClaimSchema = z
  .object({
    kind: z.string(),
    description: z.string(),
  })
  .strict();

const journeyTriggerSchema = z
  .object({
    kind: z.string(),
    description: z.string(),
  })
  .strict();

const journeyBehaviorSchema = z
  .object({
    kind: z.string(),
    description: z.string(),
  })
  .strict();

export const oracleAdapterRefSchema = z
  .object({
    kind: z.enum(["persistence", "side-effect", "detector"]),
    id: z.string(),
    version: z.string().optional(),
  })
  .strict();

export const journeyBranchSchema = z
  .object({
    id: z.string(),
    status: z.enum(["inferred", "confirmed"]),
    actor: journeyActorSchema,
    preconditions: z.array(journeyClaimSchema),
    trigger: journeyTriggerSchema,
    expectedPostconditions: z.array(journeyClaimSchema),
    persistedStateChanges: z.array(journeyClaimSchema),
    externalSideEffects: z.array(journeyClaimSchema),
    failureBehavior: journeyBehaviorSchema,
    idempotencyRetryBehavior: journeyBehaviorSchema,
    recoveryReloadBehavior: journeyBehaviorSchema,
    ownedDimensions: z.array(z.string()),
    oracleAdapters: z.array(oracleAdapterRefSchema),
    provenance: z.record(z.string(), truthSourceSchema),
  })
  .strict();

export type JourneyBranch = z.infer<typeof journeyBranchSchema>;
export type OracleAdapterRef = z.infer<typeof oracleAdapterRefSchema>;

export const semanticActionKindSchema = z.enum([
  "click",
  "fill",
  "focus",
  "press",
  "wait",
  "wait-selector",
]);

export const semanticActionSchema = z
  .object({
    id: z.string(),
    kind: semanticActionKindSchema,
    target: z.string().optional(),
    params: z.record(z.string(), z.string()).optional(),
  })
  .strict();

export const promiseDimensionSchema = z.enum([
  "accessible",
  "dom",
  "styles",
  "url",
  "storage",
  "network",
  "focus",
  "hitTest",
]);

export const promiseExpectationOperatorSchema = z.enum(["exists", "absent", "equals"]);

const unsafeObservationPathSegments = new Set(["__proto__", "prototype", "constructor"]);

function isNormalizedJsonPointer(path: string): boolean {
  if (!path.startsWith("/") || path.length === 1) {
    return false;
  }

  return path
    .slice(1)
    .split("/")
    .every(
      (segment) =>
        segment.length > 0 &&
        /^(?:[^~/]|~[01])+$/.test(segment) &&
        !unsafeObservationPathSegments.has(segment),
    );
}

export const promiseExpectationSchema = z
  .object({
    id: z.string().regex(/^[A-Za-z][A-Za-z0-9._-]*$/),
    dimension: promiseDimensionSchema,
    path: z.string().refine(isNormalizedJsonPointer, {
      message: "must be a non-empty normalized safe JSON Pointer",
    }),
    operator: promiseExpectationOperatorSchema,
    value: z.json().optional(),
  })
  .strict()
  .superRefine((expectation, ctx) => {
    const hasValue = Object.hasOwn(expectation, "value");
    if (expectation.operator === "equals" && !hasValue) {
      ctx.addIssue({
        code: "custom",
        path: ["value"],
        message: "is required when operator is equals",
      });
    }
    if (expectation.operator !== "equals" && hasValue) {
      ctx.addIssue({
        code: "custom",
        path: ["value"],
        message: "is allowed only when operator is equals",
      });
    }
  });

export const promiseContractSchema = z
  .object({
    outcome: z.string(),
    expectations: z.array(promiseExpectationSchema).min(1),
  })
  .strict()
  .superRefine((contract, ctx) => {
    const expectationIds = new Set<string>();
    const observationKeys = new Set<string>();
    for (const [index, expectation] of contract.expectations.entries()) {
      if (expectationIds.has(expectation.id)) {
        ctx.addIssue({
          code: "custom",
          path: ["expectations", index, "id"],
          message: "must be unique",
        });
      }
      expectationIds.add(expectation.id);

      const observationKey = `${expectation.dimension}\u0000${expectation.path}`;
      if (observationKeys.has(observationKey)) {
        ctx.addIssue({
          code: "custom",
          path: ["expectations", index, "path"],
          message: "must be unique within its dimension",
        });
      }
      observationKeys.add(observationKey);
    }
  });

export const uiJourneyPlanSchema = z
  .object({
    journey: journeyBranchSchema,
    semanticActions: z.array(semanticActionSchema),
    promise: promiseContractSchema,
    oracleRefs: z.array(oracleAdapterRefSchema),
  })
  .strict()
  .superRefine((plan, ctx) => {
    if (plan.oracleRefs.length !== plan.journey.oracleAdapters.length) {
      ctx.addIssue({
        code: "custom",
        path: ["oracleRefs"],
        message: "must exactly match journey.oracleAdapters",
      });
      return;
    }
    for (const [index, reference] of plan.oracleRefs.entries()) {
      const adapter = plan.journey.oracleAdapters[index];
      if (
        adapter === undefined ||
        adapter.kind !== reference.kind ||
        adapter.id !== reference.id ||
        adapter.version !== reference.version
      ) {
        ctx.addIssue({
          code: "custom",
          path: ["oracleRefs", index],
          message: "must exactly match journey.oracleAdapters in declared order",
        });
      }
    }
  });

export type SemanticAction = z.infer<typeof semanticActionSchema>;
export type PromiseDimension = z.infer<typeof promiseDimensionSchema>;
export type PromiseExpectationOperator = z.infer<typeof promiseExpectationOperatorSchema>;
export type PromiseExpectation = z.infer<typeof promiseExpectationSchema>;
export type PromiseContract = z.infer<typeof promiseContractSchema>;
export type UiJourneyPlan = z.infer<typeof uiJourneyPlanSchema>;

export type UiJourneyDeclaration = {
  id: string;
  status: "inferred" | "confirmed";
  appliesTo: { routes: string[]; roles?: string[] };
  adapter: { module: string; export: string };
};

export type UiJourneyUnavailable = { applicable: false; reason: string };

export function parseJourneyBranch(value: unknown): JourneyBranch {
  const result = journeyBranchSchema.safeParse(value);
  if (!result.success) {
    const issue = result.error.issues[0];
    throw new Error(
      `journey branch: ${issue?.path.join(".") ?? "$"}: ${issue?.message ?? "validation failed"}`,
    );
  }
  return result.data;
}

export function parseUiJourneyPlan(value: unknown): UiJourneyPlan {
  const result = uiJourneyPlanSchema.safeParse(value);
  if (!result.success) {
    const issue = result.error.issues[0];
    throw new Error(
      `ui journey plan: ${issue?.path.join(".") ?? "$"}: ${issue?.message ?? "validation failed"}`,
    );
  }
  return result.data;
}
