import path from 'node:path';

const MANIFEST_VERSION = 1;
const MAX_TITLE_LENGTH = 200;
const MAX_ENTRY_LENGTH = 260;
const MAX_SOURCE_SKILL_ID_LENGTH = 128;
const MAX_DESIGN_SYSTEM_ID_LENGTH = 128;
const MAX_SUPPORTING_FILE_LENGTH = 260;
const MAX_SUPPORTING_FILES = 128;
const MAX_METADATA_BYTES = 16 * 1024;

type JsonRecord = Record<string, unknown>;

type ValidationResult =
  | { ok: true; value: JsonRecord | null }
  | { ok: false; error: string };

type ValidationOptions = {
  preserveUpdatedAt?: boolean;
};

const ALLOWED_KINDS = new Set<string>([
  'html',
  'deck',
  'react-component',
  'markdown-document',
  'svg',
  'diagram',
  'code-snippet',
  'mini-app',
  'design-system',
]);

const ALLOWED_RENDERERS = new Set<string>([
  'html',
  'deck-html',
  'react-component',
  'markdown',
  'svg',
  'diagram',
  'code',
  'mini-app',
  'design-system',
]);

const ALLOWED_EXPORTS = new Set<string>(['html', 'pdf', 'zip', 'pptx', 'jsx', 'md', 'svg', 'txt']);
const ALLOWED_STATUS = new Set<string>(['streaming', 'complete', 'error']);

function isPlainObject(value: unknown): value is JsonRecord {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
}

function validateBoundedString(
  value: unknown,
  field: string,
  maxLen: number,
  { allowEmpty = false }: { allowEmpty?: boolean } = {},
): string | null {
  if (typeof value !== 'string') return `${field} must be a string`;
  if (!allowEmpty && value.length === 0) return `${field} is required`;
  if (value.length > maxLen) return `${field} exceeds max length (${maxLen})`;
  return null;
}

function validateSupportingPath(value: unknown): string | null {
  if (typeof value !== 'string') return 'supportingFiles entries must be strings';
  if (value.length === 0) return 'supportingFiles entries cannot be empty';
  if (value.length > MAX_SUPPORTING_FILE_LENGTH) {
    return `supportingFiles entries exceed max length (${MAX_SUPPORTING_FILE_LENGTH})`;
  }
  if (/^[A-Za-z]:/.test(value) || value.startsWith('/')) {
    return 'supportingFiles cannot contain absolute paths';
  }
  if (value.includes('\u0000')) return 'supportingFiles cannot contain null bytes';
  const normalized = value.replace(/\\/g, '/');
  if (normalized.includes('..')) return 'supportingFiles cannot contain traversal segments';
  const parts = normalized.split('/').filter(Boolean);
  if (parts.length === 0 || parts.some((p) => p === '.' || p === '..')) {
    return 'supportingFiles cannot contain traversal segments';
  }
  return null;
}

export function validateArtifactManifestInput(
  manifest: unknown,
  entry: unknown,
  options: ValidationOptions = {},
): ValidationResult {
  if (manifest == null) return { ok: true, value: null };
  if (!isPlainObject(manifest)) {
    return { ok: false, error: 'artifactManifest must be an object' };
  }

  const kindErr = validateBoundedString(manifest.kind, 'artifactManifest.kind', 64);
  if (kindErr) return { ok: false, error: kindErr };
  if (typeof manifest.kind !== 'string') {
    return { ok: false, error: 'artifactManifest.kind must be a string' };
  }
  if (!ALLOWED_KINDS.has(manifest.kind)) {
    return { ok: false, error: 'artifactManifest.kind is not allowed' };
  }

  const rendererErr = validateBoundedString(manifest.renderer, 'artifactManifest.renderer', 64);
  if (rendererErr) return { ok: false, error: rendererErr };
  if (typeof manifest.renderer !== 'string') {
    return { ok: false, error: 'artifactManifest.renderer must be a string' };
  }
  if (!ALLOWED_RENDERERS.has(manifest.renderer)) {
    return { ok: false, error: 'artifactManifest.renderer is not allowed' };
  }

  if (!Array.isArray(manifest.exports) || manifest.exports.length === 0) {
    return { ok: false, error: 'artifactManifest.exports must be a non-empty array' };
  }
  for (const exp of manifest.exports) {
    if (typeof exp !== 'string') {
      return { ok: false, error: 'artifactManifest.exports must contain strings' };
    }
    if (!ALLOWED_EXPORTS.has(exp)) {
      return { ok: false, error: `artifactManifest.exports contains unsupported value: ${exp}` };
    }
  }

  if (manifest.status !== undefined) {
    if (typeof manifest.status !== 'string') {
      return { ok: false, error: 'artifactManifest.status must be a string' };
    }
    if (!ALLOWED_STATUS.has(manifest.status)) {
      return { ok: false, error: 'artifactManifest.status is not allowed' };
    }
  }

  if (manifest.primary !== undefined) {
    if (manifest.primary !== true) {
      const primaryErr = validateSupportingPath(manifest.primary);
      if (primaryErr) return { ok: false, error: `artifactManifest.primary ${primaryErr}` };
    }
  }

  if (manifest.supportingFiles !== undefined) {
    if (!Array.isArray(manifest.supportingFiles)) {
      return { ok: false, error: 'artifactManifest.supportingFiles must be an array' };
    }
    if (manifest.supportingFiles.length > MAX_SUPPORTING_FILES) {
      return {
        ok: false,
        error: `artifactManifest.supportingFiles exceeds max items (${MAX_SUPPORTING_FILES})`,
      };
    }
    for (const rel of manifest.supportingFiles) {
      const relErr = validateSupportingPath(rel);
      if (relErr) return { ok: false, error: relErr };
    }
  }

  if (manifest.title !== undefined) {
    const titleErr = validateBoundedString(
      manifest.title,
      'artifactManifest.title',
      MAX_TITLE_LENGTH,
      { allowEmpty: false },
    );
    if (titleErr) return { ok: false, error: titleErr };
  }

  if (manifest.sourceSkillId !== undefined) {
    const skillErr = validateBoundedString(
      manifest.sourceSkillId,
      'artifactManifest.sourceSkillId',
      MAX_SOURCE_SKILL_ID_LENGTH,
      { allowEmpty: true },
    );
    if (skillErr) return { ok: false, error: skillErr };
  }

  if (manifest.designSystemId !== undefined && manifest.designSystemId !== null) {
    const dsErr = validateBoundedString(
      manifest.designSystemId,
      'artifactManifest.designSystemId',
      MAX_DESIGN_SYSTEM_ID_LENGTH,
      { allowEmpty: true },
    );
    if (dsErr) return { ok: false, error: dsErr };
  }

  if (manifest.metadata !== undefined) {
    if (!isPlainObject(manifest.metadata)) {
      return { ok: false, error: 'artifactManifest.metadata must be a plain object' };
    }
    const serialized = JSON.stringify(manifest.metadata);
    if (typeof serialized !== 'string') {
      return { ok: false, error: 'artifactManifest.metadata must be JSON-serializable' };
    }
    if (Buffer.byteLength(serialized, 'utf8') > MAX_METADATA_BYTES) {
      return {
        ok: false,
        error: `artifactManifest.metadata exceeds max size (${MAX_METADATA_BYTES} bytes)`,
      };
    }
  }

  const manifestEntry =
    typeof manifest.entry === 'string' && manifest.entry.trim()
      ? manifest.entry.trim()
      : entry;
  const entryErr = validateSupportingPath(manifestEntry);
  if (entryErr) {
    return { ok: false, error: `artifactManifest.entry ${entryErr}` };
  }
  const safeEntry = (manifestEntry as string).replace(/\\/g, '/');

  return { ok: true, value: sanitizeManifest(manifest, safeEntry, options) };
}

export function sanitizeManifest(
  manifest: JsonRecord,
  entry: string,
  options: ValidationOptions = {},
): JsonRecord {
  const now = new Date().toISOString();
  return {
    version: MANIFEST_VERSION,
    kind: manifest.kind,
    title: manifest.title || entry,
    entry,
    renderer: manifest.renderer,
    status: typeof manifest.status === 'string' && ALLOWED_STATUS.has(manifest.status) ? manifest.status : 'complete',
    exports: manifest.exports,
    primary:
      manifest.primary === true
        ? true
        : typeof manifest.primary === 'string'
          ? manifest.primary.replace(/\\/g, '/')
          : undefined,
    supportingFiles: Array.isArray(manifest.supportingFiles)
      ? manifest.supportingFiles.map((x) => String(x).replace(/\\/g, '/'))
      : undefined,
    createdAt: typeof manifest.createdAt === 'string' ? manifest.createdAt : now,
    updatedAt:
      options.preserveUpdatedAt && typeof manifest.updatedAt === 'string'
        ? manifest.updatedAt
        : now,
    sourceSkillId: manifest.sourceSkillId,
    designSystemId: manifest.designSystemId ?? undefined,
    metadata: manifest.metadata,
  };
}

export function parsePersistedManifest(raw: string, fallbackEntry: string): JsonRecord | null {
  try {
    const parsed = JSON.parse(raw);
    if (!parsed || parsed.version !== MANIFEST_VERSION) return null;
    const entry = typeof parsed.entry === 'string' && parsed.entry ? parsed.entry : fallbackEntry;
    const result = validateArtifactManifestInput(parsed, entry, { preserveUpdatedAt: true });
    return result.ok ? result.value : null;
  } catch {
    return null;
  }
}

export function inferLegacyManifest(entry: string): JsonRecord | null {
  const lower = entry.toLowerCase();
  const ext = path.extname(lower);
  // NOTE: This duplicate heuristic must stay in sync with
  // src/artifacts/manifest.ts::inferLegacyManifest() until frontend+daemon
  // inference is moved to a shared runtime-safe module.
  const isDeck = ext === '.html' && (lower.includes('deck') || lower.includes('slides') || lower.includes('pitch'));
  if (ext === '.html' || ext === '.htm') {
    return {
      version: MANIFEST_VERSION,
      kind: isDeck ? 'deck' : 'html',
      title: entry,
      entry,
      renderer: isDeck ? 'deck-html' : 'html',
      status: 'complete',
      exports: isDeck ? ['html', 'pdf', 'pptx', 'zip'] : ['html', 'pdf', 'zip'],
      metadata: { inferred: true },
    };
  }

  if (ext === '.md') {
    return {
      version: MANIFEST_VERSION,
      kind: 'markdown-document',
      title: entry,
      entry,
      renderer: 'markdown',
      status: 'complete',
      exports: ['md', 'html', 'pdf', 'zip'],
      metadata: { inferred: true },
    };
  }
  if (ext === '.svg') {
    return {
      version: MANIFEST_VERSION,
      kind: 'svg',
      title: entry,
      entry,
      renderer: 'svg',
      status: 'complete',
      exports: ['svg', 'zip'],
      metadata: { inferred: true },
    };
  }
  return null;
}
