// src/core/require/manifest.ts
// Package manifest schema and validation.

import * as fs from "fs";

const MANIFEST_NAME_PATTERN = /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)*$/;

export interface PackageManifest {
  // Required architecture fields.
  name: string;
  description: string;
  exports: string[];
  effects: string[];
  requires: string[];

  // Optional architecture extensions.
  strategy?: string;
  backends?: string[];
  helpTopics?: string[];
  configKeys?: string[];
  middleware?: {
    type: "security" | "observation" | string;
    autoInsert?: boolean;
  };

  // Optional metadata.
  version?: string;
  author?: string;
}

export type ManifestRuleId =
  | "MF-001"
  | "MF-002"
  | "MF-003"
  | "MF-004"
  | "MF-005"
  | "MF-006"
  | "MF-007"
  | "MF-008"
  | "MF-009"
  | "MF-010"
  | "MF-011"
  | "MF-012"
  | "MF-013";

export interface ManifestDiagnostic {
  ruleId: ManifestRuleId;
  field?: string;
  message: string;
}

export interface ManifestValidationResult {
  valid: boolean;
  errors: ManifestDiagnostic[];
  warnings: ManifestDiagnostic[];
}

function toStringArray(value: unknown): string[] {
  if (Array.isArray(value)) {
    return value.map((item) => String(item)).map((item) => item.trim()).filter(Boolean);
  }
  if (value === undefined || value === null) {
    return [];
  }
  const single = String(value).trim();
  return single ? [single] : [];
}

export function normalizePackageManifest(input: unknown): PackageManifest {
  const data = (input ?? {}) as Record<string, unknown>;

  const strategyValue = data.strategy;
  let strategy: string | undefined;
  if (typeof strategyValue === "string") {
    strategy = strategyValue.trim() || undefined;
  } else if (strategyValue !== undefined) {
    strategy = strategyValue as any;
  }

  let middleware: PackageManifest["middleware"];
  if (data.middleware && typeof data.middleware === "object" && !Array.isArray(data.middleware)) {
    const raw = data.middleware as Record<string, unknown>;
    middleware = {
      type: String(raw.type ?? "").trim(),
      autoInsert: typeof raw.autoInsert === "boolean" ? raw.autoInsert : (raw.autoInsert as any),
    };
  } else if (data.middleware !== undefined) {
    middleware = data.middleware as any;
  }

  return {
    name: typeof data.name === "string" ? data.name.trim() : String(data.name ?? "").trim(),
    description:
      typeof data.description === "string"
        ? data.description
        : String(data.description ?? ""),
    exports: toStringArray(data.exports),
    effects: toStringArray(data.effects),
    requires: toStringArray(data.requires),
    strategy,
    backends: toStringArray(data.backends),
    helpTopics: toStringArray(data.helpTopics ?? data.help_topics),
    configKeys: toStringArray(data.configKeys ?? data.config_keys),
    middleware,
    version: typeof data.version === "string" ? data.version : undefined,
    author: typeof data.author === "string" ? data.author : undefined,
  };
}

export function validatePackageManifest(manifest: PackageManifest): ManifestValidationResult {
  const errors: ManifestDiagnostic[] = [];
  const warnings: ManifestDiagnostic[] = [];

  if (!manifest.name || typeof manifest.name !== "string") {
    errors.push({
      ruleId: "MF-001",
      field: "name",
      message: "manifest.name must be a non-empty string",
    });
  } else if (!MANIFEST_NAME_PATTERN.test(manifest.name)) {
    errors.push({
      ruleId: "MF-001",
      field: "name",
      message:
        "manifest.name must match ^[a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)*$",
    });
  }

  if (typeof manifest.description !== "string") {
    errors.push({
      ruleId: "MF-002",
      field: "description",
      message: "manifest.description must be a string",
    });
  } else {
    const trimmed = manifest.description.trim();
    if (trimmed.length === 0) {
      errors.push({
        ruleId: "MF-002",
        field: "description",
        message: "manifest.description must be non-empty",
      });
    }
    if (trimmed.length > 120) {
      errors.push({
        ruleId: "MF-002",
        field: "description",
        message: "manifest.description must be 120 characters or fewer",
      });
    }
  }

  if (!Array.isArray(manifest.exports)) {
    errors.push({
      ruleId: "MF-003",
      field: "exports",
      message: "manifest.exports must be an array of strings",
    });
  }

  if (!Array.isArray(manifest.effects)) {
    errors.push({
      ruleId: "MF-004",
      field: "effects",
      message: "manifest.effects must be an array of strings",
    });
  }

  if (!Array.isArray(manifest.requires)) {
    errors.push({
      ruleId: "MF-005",
      field: "requires",
      message: "manifest.requires must be an array of strings",
    });
  }

  if (manifest.strategy !== undefined && typeof manifest.strategy !== "string") {
    errors.push({
      ruleId: "MF-006",
      field: "strategy",
      message: "manifest.strategy must be a string when present",
    });
  }

  for (const [field, value, ruleId] of [
    ["exports", manifest.exports, "MF-007"],
    ["effects", manifest.effects, "MF-008"],
    ["requires", manifest.requires, "MF-009"],
  ] as const) {
    if (value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
      errors.push({
        ruleId,
        field,
        message: `manifest.${field} entries must be non-empty strings`,
      });
    }
  }

  for (const [field, value] of [
    ["backends", manifest.backends],
    ["helpTopics", manifest.helpTopics],
    ["configKeys", manifest.configKeys],
  ] as const) {
    if (!value) {
      continue;
    }
    if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
      errors.push({
        ruleId: "MF-010",
        field,
        message: `manifest.${field} must be an array of non-empty strings when present`,
      });
    }
  }

  if (manifest.middleware !== undefined) {
    if (!manifest.middleware || typeof manifest.middleware !== "object" || Array.isArray(manifest.middleware)) {
      errors.push({
        ruleId: "MF-012",
        field: "middleware",
        message: "manifest.middleware must be an object when present",
      });
    } else {
      const middlewareType = String(manifest.middleware.type ?? "").trim();
      if (middlewareType !== "security" && middlewareType !== "observation") {
        errors.push({
          ruleId: "MF-013",
          field: "middleware.type",
          message: "manifest.middleware.type must be either \"security\" or \"observation\"",
        });
      }

      if (
        manifest.middleware.autoInsert !== undefined &&
        typeof manifest.middleware.autoInsert !== "boolean"
      ) {
        errors.push({
          ruleId: "MF-012",
          field: "middleware.autoInsert",
          message: "manifest.middleware.autoInsert must be a boolean when present",
        });
      }
    }
  }

  if (manifest.exports.length === 0) {
    warnings.push({
      ruleId: "MF-003",
      field: "exports",
      message: "manifest.exports is empty; package has no declared public symbols",
    });
  }

  return {
    valid: errors.length === 0,
    errors,
    warnings,
  };
}

function assertValidManifest(manifest: PackageManifest, context: string): void {
  const validation = validatePackageManifest(manifest);
  if (validation.valid) {
    return;
  }

  const detail = validation.errors
    .map((error) => `${error.ruleId}${error.field ? `(${error.field})` : ""}: ${error.message}`)
    .join("; ");

  throw new Error(`${context}: ${detail}`);
}

export function parseManifestObject(data: unknown, context = "manifest"): PackageManifest {
  if (data && typeof data === "object" && !Array.isArray(data)) {
    const raw = data as Record<string, unknown>;
    const missingRequired = ["name", "description", "exports", "effects", "requires"].filter(
      (key) => !(key in raw),
    );
    if (missingRequired.length > 0) {
      const details = missingRequired.map((key) => `MF-011(${key}): missing required field`).join("; ");
      throw new Error(`${context}: ${details}`);
    }
  }

  const manifest = normalizePackageManifest(data);
  assertValidManifest(manifest, context);
  return manifest;
}

export function parseManifestFile(manifestPath: string): PackageManifest {
  const content = fs.readFileSync(manifestPath, "utf8");
  const data = JSON.parse(content) as unknown;
  return parseManifestObject(data, manifestPath);
}

export class PackageRegistry {
  private readonly manifests = new Map<string, PackageManifest>();

  register(manifestInput: PackageManifest): void {
    const manifest = parseManifestObject(manifestInput, `package ${manifestInput.name ?? "<unknown>"}`);
    this.manifests.set(manifest.name, manifest);
  }

  get(name: string): PackageManifest | undefined {
    return this.manifests.get(name);
  }

  list(): string[] {
    return Array.from(this.manifests.keys()).sort();
  }

  listAll(): PackageManifest[] {
    return Array.from(this.manifests.values());
  }

  search(query: string): PackageManifest[] {
    const q = query.toLowerCase();
    return this.listAll().filter(
      (m) =>
        m.name.toLowerCase().includes(q) ||
        m.description.toLowerCase().includes(q) ||
        m.exports.some((symbol) => symbol.toLowerCase().includes(q)) ||
        m.effects.some((effect) => effect.toLowerCase().includes(q)),
    );
  }

  findProvider(symbol: string): PackageManifest[] {
    return this.listAll().filter((manifest) => manifest.exports.includes(symbol));
  }

  findByEffect(effect: string): PackageManifest[] {
    return this.listAll().filter((manifest) => manifest.effects.includes(effect));
  }

  describe(name: string): string {
    const manifest = this.manifests.get(name);
    if (!manifest) {
      return `Package not found: ${name}`;
    }

    const lines = [
      `Package: ${manifest.name}`,
      manifest.version ? `Version: ${manifest.version}` : null,
      `Description: ${manifest.description}`,
      manifest.exports.length > 0 ? `Exports: ${manifest.exports.join(", ")}` : null,
      manifest.effects.length > 0 ? `Effects: ${manifest.effects.join(", ")}` : null,
      manifest.requires.length > 0 ? `Requires: ${manifest.requires.join(", ")}` : null,
      manifest.strategy ? `Strategy: ${manifest.strategy}` : null,
      manifest.backends && manifest.backends.length > 0 ? `Backends: ${manifest.backends.join(", ")}` : null,
      manifest.helpTopics && manifest.helpTopics.length > 0 ? `Help Topics: ${manifest.helpTopics.join(", ")}` : null,
      manifest.configKeys && manifest.configKeys.length > 0 ? `Config Keys: ${manifest.configKeys.join(", ")}` : null,
      manifest.middleware ? `Middleware: ${manifest.middleware.type}` : null,
    ].filter(Boolean);

    return lines.join("\n");
  }

  clear(): void {
    this.manifests.clear();
  }

  get size(): number {
    return this.manifests.size;
  }
}

// Parse a (define-package ...) S-expression into a PackageManifest.
export function parseDefinePackage(args: any[]): PackageManifest {
  const name = typeof args[0] === "string" ? args[0] : String(args[0]);
  const manifest: Record<string, unknown> = {
    name,
    description: `Package ${name}`,
    exports: [],
    effects: [],
    requires: [],
  };

  for (let i = 1; i < args.length; i += 2) {
    const key = String(args[i]);
    const value = args[i + 1];

    switch (key) {
      case ":description":
        manifest.description = String(value);
        break;
      case ":exports":
        manifest.exports = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":effects":
        manifest.effects = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":requires":
        manifest.requires = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":strategy":
        if (typeof value === "string") {
          manifest.strategy = value;
        }
        break;
      case ":backends":
        manifest.backends = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":help-topics":
        manifest.helpTopics = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":config-keys":
        manifest.configKeys = Array.isArray(value) ? value.map(String) : [String(value)];
        break;
      case ":middleware":
        if (value && typeof value === "object" && !Array.isArray(value)) {
          const raw = value as Record<string, unknown>;
          manifest.middleware = {
            type: String(raw.type ?? ""),
            autoInsert: raw.autoInsert as boolean | undefined,
          };
        }
        break;
      case ":version":
        manifest.version = String(value);
        break;
      case ":author":
        manifest.author = String(value);
        break;
      default:
        break;
    }
  }

  return parseManifestObject(manifest, `(define-package ${name})`);
}
