import { createHash } from "node:crypto";
import { constants as fsConstants } from "node:fs";
import {
  chmod,
  lstat,
  mkdir,
  open as openFile,
  readdir,
  readFile,
  realpath
} from "node:fs/promises";
import path from "node:path";

const eventTypes = new Set([
  "activation-stage",
  "review-completed",
  "rerun-completed",
  "assurance-completed",
  "passport-exported"
]);
const durationBuckets = new Set([
  "not-measured",
  "under-30s",
  "30s-2m",
  "2m-5m",
  "5m-10m",
  "over-10m"
]);
const eventKeys = Object.freeze([
  "schemaVersion",
  "artifactType",
  "profile",
  "eventId",
  "eventType",
  "stage",
  "recordedAt",
  "weekStartedAt",
  "repoFingerprint",
  "operationBindingSha256",
  "plan",
  "platform",
  "version",
  "durationBucket",
  "boundaries",
  "checksumSha256"
]);
const boundaryKeys = Object.freeze([
  "localOnly",
  "repositoryScopeUpload",
  "sourceIncluded",
  "pathIncluded",
  "refIncluded",
  "commitIncluded",
  "findingIncluded",
  "promptIncluded",
  "diffIncluded",
  "agentOutputIncluded",
  "providerDataIncluded",
  "networkUsed",
  "authority"
]);

export const RUNRAIL_PRODUCT_METRICS = Object.freeze({
  schemaVersion: 1,
  artifactType: "runrail-product-metric-event",
  profile: "runrail-product-metrics-v1",
  summaryArtifactType: "runrail-product-metrics-summary",
  aggregateArtifactType: "runrail-product-metrics-daily-aggregate",
  outputDirectory: "product-metrics",
  maximumEvents: 100_000,
  maximumArtifactBytes: 16 * 1024,
  eventTypes: Object.freeze([...eventTypes]),
  durationBuckets: Object.freeze([...durationBuckets]),
  uploadAvailable: false,
  authority: "none"
});

export class RunrailProductMetricsError extends Error {
  constructor(code, message) {
    super(message);
    this.name = "RunrailProductMetricsError";
    this.code = code;
  }
}

function fail(code, message) {
  throw new RunrailProductMetricsError(code, message);
}

function sha256(value) {
  return createHash("sha256").update(value).digest("hex");
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === "object") {
    return Object.fromEntries(
      Object.keys(value)
        .sort()
        .map((key) => [key, stableValue(value[key])])
    );
  }
  return value;
}

export function stableRunrailProductMetricJson(value) {
  return `${JSON.stringify(stableValue(value), null, 2)}\n`;
}

function isPlainRecord(value) {
  return (
    value !== null &&
    typeof value === "object" &&
    !Array.isArray(value) &&
    Object.getPrototypeOf(value) === Object.prototype
  );
}

function exactKeys(value, expected, code = "product-metric-invalid") {
  if (!isPlainRecord(value)) fail(code, "Product metric must be a plain object.");
  const actual = Object.keys(value).sort();
  const required = [...expected].sort();
  if (
    actual.length !== required.length ||
    actual.some((key, index) => key !== required[index])
  ) {
    fail(code, "Product metric contains missing or unsupported fields.");
  }
}

function safeTimestamp(value, label = "Product metric timestamp") {
  const text = String(value || "");
  const timestamp = Date.parse(text);
  if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== text) {
    fail("product-metric-time-invalid", `${label} is invalid.`);
  }
  return text;
}

function weekStart(value) {
  const date = new Date(safeTimestamp(value));
  const day = date.getUTCDay();
  const offset = day === 0 ? 6 : day - 1;
  date.setUTCDate(date.getUTCDate() - offset);
  date.setUTCHours(0, 0, 0, 0);
  return date.toISOString();
}

function safeSlug(value, label, maximumLength = 48) {
  const text = String(value || "").toLowerCase();
  if (
    text.length < 1 ||
    text.length > maximumLength ||
    !/^[a-z0-9][a-z0-9._-]*$/.test(text)
  ) {
    fail("product-metric-field-invalid", `${label} is invalid.`);
  }
  return text;
}

function safeRepoFingerprint(value) {
  if (value === null) return null;
  const text = String(value || "");
  if (!/^RRF-[A-F0-9]{64}$/.test(text)) {
    fail(
      "product-metric-repository-invalid",
      "Product metric repository fingerprint is invalid."
    );
  }
  return text;
}

function safeChecksum(value, { nullable = false } = {}) {
  if (nullable && value === null) return null;
  const text = String(value || "").toLowerCase();
  if (!/^[a-f0-9]{64}$/.test(text)) {
    fail("product-metric-checksum-invalid", "Product metric checksum is invalid.");
  }
  return text;
}

function expectedBoundaries() {
  return {
    localOnly: true,
    repositoryScopeUpload: false,
    sourceIncluded: false,
    pathIncluded: false,
    refIncluded: false,
    commitIncluded: false,
    findingIncluded: false,
    promptIncluded: false,
    diffIncluded: false,
    agentOutputIncluded: false,
    providerDataIncluded: false,
    networkUsed: false,
    authority: "none"
  };
}

function semanticEventIdentity(event) {
  return stableRunrailProductMetricJson({
    eventType: event.eventType,
    stage: event.stage,
    repoFingerprint: event.repoFingerprint,
    operationBindingSha256: event.operationBindingSha256
  });
}

export function runrailDurationBucket(durationMilliseconds) {
  if (
    durationMilliseconds === null ||
    durationMilliseconds === undefined ||
    !Number.isFinite(durationMilliseconds) ||
    durationMilliseconds < 0
  ) {
    return "not-measured";
  }
  if (durationMilliseconds < 30_000) return "under-30s";
  if (durationMilliseconds < 120_000) return "30s-2m";
  if (durationMilliseconds < 300_000) return "2m-5m";
  if (durationMilliseconds < 600_000) return "5m-10m";
  return "over-10m";
}

export function createRunrailProductMetricEvent({
  eventType,
  stage,
  recordedAt,
  repoFingerprint = null,
  operationBinding = null,
  plan = "preview",
  platform = "unknown",
  version,
  durationMilliseconds = null
}) {
  const safeType = String(eventType || "");
  if (!eventTypes.has(safeType)) {
    fail("product-metric-type-invalid", "Product metric event type is invalid.");
  }
  const safeStage = safeSlug(stage, "Product metric stage", 64);
  const safeRecordedAt = safeTimestamp(recordedAt);
  const safeRepo = safeRepoFingerprint(repoFingerprint);
  const operationBindingSha256 =
    operationBinding === null
      ? null
      : sha256(String(operationBinding));
  if (
    safeType !== "activation-stage" &&
    (!safeRepo || !operationBindingSha256)
  ) {
    fail(
      "product-metric-binding-invalid",
      "Repository product events require local repository and operation bindings."
    );
  }
  const body = {
    schemaVersion: RUNRAIL_PRODUCT_METRICS.schemaVersion,
    artifactType: RUNRAIL_PRODUCT_METRICS.artifactType,
    profile: RUNRAIL_PRODUCT_METRICS.profile,
    eventId: `RPM-${sha256(
      semanticEventIdentity({
        eventType: safeType,
        stage: safeStage,
        repoFingerprint: safeRepo,
        operationBindingSha256
      })
    )
      .slice(0, 32)
      .toUpperCase()}`,
    eventType: safeType,
    stage: safeStage,
    recordedAt: safeRecordedAt,
    weekStartedAt: weekStart(safeRecordedAt),
    repoFingerprint: safeRepo,
    operationBindingSha256,
    plan: safeSlug(plan, "Product metric plan", 32),
    platform: safeSlug(platform, "Product metric platform", 32),
    version: safeSlug(version, "Product metric version", 32),
    durationBucket: runrailDurationBucket(durationMilliseconds),
    boundaries: expectedBoundaries()
  };
  return verifyRunrailProductMetricEvent({
    ...body,
    checksumSha256: sha256(stableRunrailProductMetricJson(body))
  });
}

export function verifyRunrailProductMetricEvent(event) {
  exactKeys(event, eventKeys);
  if (
    event.schemaVersion !== RUNRAIL_PRODUCT_METRICS.schemaVersion ||
    event.artifactType !== RUNRAIL_PRODUCT_METRICS.artifactType ||
    event.profile !== RUNRAIL_PRODUCT_METRICS.profile ||
    !/^RPM-[A-F0-9]{32}$/.test(String(event.eventId || "")) ||
    !eventTypes.has(event.eventType) ||
    !durationBuckets.has(event.durationBucket)
  ) {
    fail("product-metric-invalid", "Product metric identity is invalid.");
  }
  safeSlug(event.stage, "Product metric stage", 64);
  safeTimestamp(event.recordedAt);
  if (event.weekStartedAt !== weekStart(event.recordedAt)) {
    fail("product-metric-time-invalid", "Product metric week is invalid.");
  }
  const safeRepo = safeRepoFingerprint(event.repoFingerprint);
  const safeBinding = safeChecksum(event.operationBindingSha256, {
    nullable: true
  });
  if (
    event.eventType !== "activation-stage" &&
    (!safeRepo || !safeBinding)
  ) {
    fail(
      "product-metric-binding-invalid",
      "Repository product event binding is invalid."
    );
  }
  safeSlug(event.plan, "Product metric plan", 32);
  safeSlug(event.platform, "Product metric platform", 32);
  safeSlug(event.version, "Product metric version", 32);
  exactKeys(event.boundaries, boundaryKeys, "product-metric-boundary-invalid");
  const boundaries = expectedBoundaries();
  if (
    boundaryKeys.some((key) => event.boundaries[key] !== boundaries[key])
  ) {
    fail("product-metric-boundary-invalid", "Product metric expands authority.");
  }
  const expectedId = `RPM-${sha256(semanticEventIdentity(event))
    .slice(0, 32)
    .toUpperCase()}`;
  if (event.eventId !== expectedId) {
    fail("product-metric-invalid", "Product metric ID is invalid.");
  }
  const { checksumSha256, ...body } = event;
  if (
    safeChecksum(checksumSha256) !==
    sha256(stableRunrailProductMetricJson(body))
  ) {
    fail("product-metric-tampered", "Product metric checksum is invalid.");
  }
  return event;
}

function inside(root, candidate) {
  const relative = path.relative(root, candidate);
  return (
    relative !== "" &&
    relative !== ".." &&
    !relative.startsWith(`..${path.sep}`) &&
    !path.isAbsolute(relative)
  );
}

async function metricsRoot(historyRoot, { create = true } = {}) {
  const root = path.resolve(historyRoot);
  let rootEntry = await lstat(root).catch(() => null);
  if (!rootEntry && create) {
    await mkdir(root, { recursive: true, mode: 0o700 });
    rootEntry = await lstat(root);
  }
  if (
    !rootEntry ||
    !rootEntry.isDirectory() ||
    rootEntry.isSymbolicLink() ||
    await realpath(root) !== root
  ) {
    fail(
      "product-metric-root-invalid",
      "Product metric history root is not an ordinary directory."
    );
  }
  const outputRoot = path.join(
    root,
    RUNRAIL_PRODUCT_METRICS.outputDirectory
  );
  if (!inside(root, outputRoot)) {
    fail("product-metric-path-invalid", "Product metric path escaped history.");
  }
  let outputEntry = await lstat(outputRoot).catch(() => null);
  if (!outputEntry && !create) return null;
  if (!outputEntry && create) {
    await mkdir(outputRoot, { mode: 0o700 });
    outputEntry = await lstat(outputRoot);
  }
  if (
    !outputEntry ||
    !outputEntry.isDirectory() ||
    outputEntry.isSymbolicLink() ||
    await realpath(outputRoot) !== outputRoot
  ) {
    fail("product-metric-path-invalid", "Product metric directory is unsafe.");
  }
  return outputRoot;
}

async function listEventFiles(outputRoot) {
  const dateNames = (await readdir(outputRoot))
    .filter((name) => /^\d{4}-\d{2}-\d{2}$/.test(name))
    .sort();
  const files = [];
  for (const dateName of dateNames) {
    const dateRoot = path.join(outputRoot, dateName);
    const entry = await lstat(dateRoot);
    if (
      !entry.isDirectory() ||
      entry.isSymbolicLink() ||
      await realpath(dateRoot) !== dateRoot
    ) {
      fail("product-metric-path-invalid", "Product metric date directory is unsafe.");
    }
    const names = (await readdir(dateRoot))
      .filter((name) => /^RPM-[A-F0-9]{32}\.json$/.test(name))
      .sort();
    for (const name of names) files.push(path.join(dateRoot, name));
  }
  if (files.length > RUNRAIL_PRODUCT_METRICS.maximumEvents) {
    fail("product-metric-limit-reached", "Product metric event limit was reached.");
  }
  return files;
}

export async function writeRunrailProductMetricEvent({
  historyRoot,
  event
}) {
  const verified = verifyRunrailProductMetricEvent(event);
  const outputRoot = await metricsRoot(historyRoot);
  const existingFiles = await listEventFiles(outputRoot);
  const existingMatch = existingFiles.find(
    (candidate) => path.basename(candidate) === `${verified.eventId}.json`
  );
  if (existingMatch) {
    const current = verifyRunrailProductMetricEvent(
      JSON.parse(await readFile(existingMatch, "utf8"))
    );
    const semanticFields = [
      "eventId",
      "eventType",
      "stage",
      "repoFingerprint",
      "operationBindingSha256",
      "plan",
      "platform",
      "version",
      "durationBucket"
    ];
    if (
      semanticFields.some((key) => current[key] !== verified[key])
    ) {
      fail(
        "product-metric-artifact-conflict",
        "Product metric conflicts with its immutable local event."
      );
    }
    return { event: current, eventPath: existingMatch, created: false };
  }
  if (existingFiles.length >= RUNRAIL_PRODUCT_METRICS.maximumEvents) {
    fail("product-metric-limit-reached", "Product metric event limit was reached.");
  }
  const dateName = verified.recordedAt.slice(0, 10);
  const dateRoot = path.join(outputRoot, dateName);
  if (!inside(outputRoot, dateRoot)) {
    fail("product-metric-path-invalid", "Product metric date path escaped.");
  }
  const dateEntry = await lstat(dateRoot).catch(() => null);
  if (!dateEntry) await mkdir(dateRoot, { mode: 0o700 });
  const verifiedDateEntry = await lstat(dateRoot);
  if (
    !verifiedDateEntry.isDirectory() ||
    verifiedDateEntry.isSymbolicLink() ||
    await realpath(dateRoot) !== dateRoot
  ) {
    fail("product-metric-path-invalid", "Product metric date directory is unsafe.");
  }
  const eventPath = path.join(dateRoot, `${verified.eventId}.json`);
  if (!inside(dateRoot, eventPath)) {
    fail("product-metric-path-invalid", "Product metric event path escaped.");
  }
  const handle = await openFile(
    eventPath,
    fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
    0o600
  );
  try {
    await handle.writeFile(stableRunrailProductMetricJson(verified), "utf8");
    await handle.sync();
  } finally {
    await handle.close();
  }
  if (process.platform !== "win32") await chmod(eventPath, 0o600);
  return { event: verified, eventPath, created: true };
}

export async function listRunrailProductMetricEvents({ historyRoot }) {
  const outputRoot = await metricsRoot(historyRoot, { create: false });
  if (!outputRoot) return [];
  const files = await listEventFiles(outputRoot);
  const events = [];
  for (const eventPath of files) {
    const entry = await lstat(eventPath);
    if (
      !entry.isFile() ||
      entry.isSymbolicLink() ||
      entry.size < 2 ||
      entry.size > RUNRAIL_PRODUCT_METRICS.maximumArtifactBytes ||
      await realpath(eventPath) !== eventPath
    ) {
      fail("product-metric-artifact-invalid", "Product metric event is unsafe.");
    }
    const event = verifyRunrailProductMetricEvent(
      JSON.parse(await readFile(eventPath, "utf8"))
    );
    if (`${event.eventId}.json` !== path.basename(eventPath)) {
      fail("product-metric-artifact-invalid", "Product metric filename is invalid.");
    }
    events.push(event);
  }
  return events.sort(
    (left, right) =>
      left.recordedAt.localeCompare(right.recordedAt) ||
      left.eventId.localeCompare(right.eventId)
  );
}

function countValues(rows, key) {
  const counts = {};
  for (const row of rows) {
    const value = row[key];
    counts[value] = (counts[value] || 0) + 1;
  }
  return Object.fromEntries(
    Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))
  );
}

function completedAssuranceEvents(events) {
  const unique = new Map();
  for (const event of events) {
    if (
      event.eventType === "assurance-completed" &&
      event.stage === "ready-for-human-decision" &&
      event.repoFingerprint &&
      event.operationBindingSha256
    ) {
      unique.set(event.operationBindingSha256, event);
    }
  }
  return [...unique.values()];
}

export function buildRunrailProductMetricsSummary({
  events,
  asOf = new Date().toISOString()
}) {
  if (
    !Array.isArray(events) ||
    events.length > RUNRAIL_PRODUCT_METRICS.maximumEvents
  ) {
    fail("product-metric-summary-invalid", "Product metric summary input is invalid.");
  }
  const verified = events.map(verifyRunrailProductMetricEvent);
  const weekStartedAt = weekStart(asOf);
  const weekEnd = new Date(weekStartedAt);
  weekEnd.setUTCDate(weekEnd.getUTCDate() + 7);
  const weekly = verified.filter((event) => {
    const timestamp = Date.parse(event.recordedAt);
    return timestamp >= Date.parse(weekStartedAt) && timestamp < weekEnd.getTime();
  });
  const activeRepositories = new Set(
    weekly
      .filter((event) =>
        ["review-completed", "rerun-completed", "assurance-completed"].includes(
          event.eventType
        )
      )
      .map((event) => event.repoFingerprint)
      .filter(Boolean)
  );
  const completedLoops = completedAssuranceEvents(weekly);
  const dailyGroups = new Map();
  for (const event of verified) {
    const day = event.recordedAt.slice(0, 10);
    if (!dailyGroups.has(day)) dailyGroups.set(day, []);
    dailyGroups.get(day).push(event);
  }
  const dailyAggregates = [...dailyGroups.entries()]
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([date, rows]) => ({
      date,
      eventCount: rows.length,
      eventCounts: countValues(rows, "eventType"),
      durationBuckets: countValues(rows, "durationBucket"),
      plans: countValues(rows, "plan"),
      platforms: countValues(rows, "platform"),
      versions: countValues(rows, "version"),
      completedAssuranceLoopCount: completedAssuranceEvents(rows).length
    }));
  return {
    schemaVersion: RUNRAIL_PRODUCT_METRICS.schemaVersion,
    artifactType: RUNRAIL_PRODUCT_METRICS.summaryArtifactType,
    profile: RUNRAIL_PRODUCT_METRICS.profile,
    asOf: safeTimestamp(asOf, "Product metric summary timestamp"),
    weekStartedAt,
    northStar: {
      metric: "completed-assurance-loops-per-active-repository-per-week",
      activeRepositoryCount: activeRepositories.size,
      completedAssuranceLoopCount: completedLoops.length,
      loopsPerActiveRepository:
        activeRepositories.size === 0
          ? 0
          : Number(
              (completedLoops.length / activeRepositories.size).toFixed(4)
            )
    },
    weeklyEventCounts: countValues(weekly, "eventType"),
    dailyAggregates,
    upload: {
      available: false,
      enabled: false,
      consentRequired: true,
      consentGranted: false,
      dailyAggregateOnly: true,
      repositoryFingerprintIncluded: false,
      operationBindingIncluded: false,
      sourceOrDiffIncluded: false,
      networkUsed: false
    },
    boundaries: {
      localEventLedger: true,
      aggregatePathFree: true,
      aggregateRepositoryIdentityFree: true,
      aggregateSourceFree: true,
      aggregateProviderDataFree: true,
      telemetryDefaultOff: true,
      uploadImplemented: false,
      networkUsed: false,
      authority: "none"
    }
  };
}

export async function loadRunrailProductMetricsSummary({
  historyRoot,
  asOf = new Date().toISOString()
}) {
  return buildRunrailProductMetricsSummary({
    events: await listRunrailProductMetricEvents({ historyRoot }),
    asOf
  });
}
