/**
 * Executive CV generation — AI output normalisation.
 *
 * The `generatedCV` sub-schema (models/ExecutiveCV.ts) enforces `required` fields
 * and enums on every nested entry: `id` on each experience/project/media row,
 * `source` on projects, `employmentType` / interview / speaking `type` enums,
 * and so on. A language model routinely omits the synthetic ones (ids, `source`)
 * or returns an enum in prose casing ("Full Time", "Co-Founder"), and Mongoose
 * then rejects `cv.save()` with a ValidationError AFTER the AI call has already
 * succeeded — surfacing to the user as a generic "missing required information".
 *
 * This module makes the AI payload schema-safe without inventing content:
 * synthetic fields are filled, enums are coerced to their allowed value, and an
 * entry that is genuinely unusable (no company/role/name at all) is dropped
 * rather than failing the whole generation. Nothing here changes the CV's
 * meaning — only its shape.
 */

import { randomUUID } from 'crypto';

const EMPLOYMENT_TYPES = ['full-time', 'part-time', 'contract', 'freelance', 'internship', 'co-founder'];
const INTERVIEW_TYPES = ['podcast', 'tv', 'media-interview', 'panel-discussion'];
const SPEAKING_TYPES = ['keynote', 'conference', 'workshop', 'guest-lecture', 'panel'];
const PROJECT_SOURCES = ['imported', 'manual'];

export interface NormalizeFounderLike {
  name?: string;
  designation?: string;
  bio?: string;
  email?: string;
  phone?: string;
  socialProfiles?: Record<string, string> | null;
}

const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v).trim());

/** Slugify a loose enum value ("Full Time", "Co_Founder") and match it against the allowed set. */
function coerceEnum(value: unknown, allowed: string[], fallback?: string): string | undefined {
  const slug = text(value).toLowerCase().replace(/[\s_]+/g, '-');
  if (allowed.includes(slug)) return slug;
  return fallback;
}

/** Ensure every entry carries the synthetic `id` the sub-schemas require. */
function withId<T extends Record<string, any>>(entry: T): T {
  return text(entry?.id) ? entry : { ...entry, id: randomUUID() };
}

function asArray(value: unknown): any[] {
  return Array.isArray(value) ? value.filter((e) => e && typeof e === 'object') : [];
}

/**
 * Make an AI-generated CV satisfy the GeneratedCV sub-schema.
 *
 * @param raw     Parsed AI JSON.
 * @param founder The founder record the CV was generated from (backfill source).
 * @returns       The normalised CV plus the entries that had to be dropped, so
 *                the caller can log what the model failed to provide.
 */
export function normalizeGeneratedCV(
  raw: any,
  founder?: NormalizeFounderLike | null,
): { cv: any; dropped: string[] } {
  const dropped: string[] = [];
  const cv = { ...(raw && typeof raw === 'object' ? raw : {}) };

  // ── personalInfo — backfill from the founder record the CV was built from ──
  const info = { ...(cv.personalInfo || {}) };
  info.name = text(info.name) || text(founder?.name);
  info.designation = text(info.designation) || text(founder?.designation);
  // `bio` is required by the schema; fall back to the founder's own bio and then
  // to the designation, which pre-flight validation guarantees is present.
  info.bio = text(info.bio) || text(founder?.bio) || info.designation;
  info.email = text(info.email) || text(founder?.email);
  info.phone = text(info.phone) || text(founder?.phone);
  info.linkedin = text(info.linkedin) || text(founder?.socialProfiles?.linkedIn);
  cv.personalInfo = info;

  // ── experience — id + employmentType enum; company/role/startDate required ──
  cv.experience = asArray(cv.experience)
    .map((e, i) => {
      if (!text(e.company) || !text(e.role) || !text(e.startDate)) {
        dropped.push(`experience[${i}]`);
        return null;
      }
      const entry = withId(e);
      const employmentType = coerceEnum(entry.employmentType, EMPLOYMENT_TYPES);
      // An unrecognised value must be removed, not left in place: the field is
      // optional, but an off-enum string fails validation.
      if (employmentType) entry.employmentType = employmentType;
      else delete entry.employmentType;
      return entry;
    })
    .filter(Boolean);

  // ── projects — id + required `source` enum; name/description/role required ──
  cv.projects = asArray(cv.projects)
    .map((p, i) => {
      if (!text(p.name) || !text(p.description) || !text(p.role)) {
        dropped.push(`projects[${i}]`);
        return null;
      }
      return { ...withId(p), source: coerceEnum(p.source, PROJECT_SOURCES, 'manual') };
    })
    .filter(Boolean);

  // ── education / certifications — all three fields are required per entry ──
  cv.education = asArray(cv.education).filter((e, i) => {
    const ok = text(e.institution) && text(e.degree) && text(e.year);
    if (!ok) dropped.push(`education[${i}]`);
    return ok;
  });
  cv.certifications = asArray(cv.certifications).filter((c, i) => {
    const ok = text(c.name) && text(c.issuer) && text(c.year);
    if (!ok) dropped.push(`certifications[${i}]`);
    return ok;
  });

  // ── mediaPresence — id, required type enum, title/event, date ──
  const media = { ...(cv.mediaPresence || {}) };
  media.interviews = asArray(media.interviews)
    .map((m, i) => {
      if (!text(m.title) || !text(m.date)) {
        dropped.push(`interviews[${i}]`);
        return null;
      }
      return { ...withId(m), type: coerceEnum(m.type, INTERVIEW_TYPES, 'media-interview') };
    })
    .filter(Boolean);
  media.speaking = asArray(media.speaking)
    .map((s, i) => {
      if (!text(s.title) || !text(s.event) || !text(s.date)) {
        dropped.push(`speaking[${i}]`);
        return null;
      }
      return { ...withId(s), type: coerceEnum(s.type, SPEAKING_TYPES, 'conference') };
    })
    .filter(Boolean);
  cv.mediaPresence = media;

  // ── fundingTrackRecord / presentations — id + required text fields ──
  cv.fundingTrackRecord = asArray(cv.fundingTrackRecord)
    .map((f, i) => {
      if (!text(f.roundType) || !text(f.amount) || !text(f.date)) {
        dropped.push(`fundingTrackRecord[${i}]`);
        return null;
      }
      return withId(f);
    })
    .filter(Boolean);
  cv.presentations = asArray(cv.presentations)
    .map((p, i) => {
      if (!text(p.title) || !text(p.type) || !text(p.date)) {
        dropped.push(`presentations[${i}]`);
        return null;
      }
      return withId(p);
    })
    .filter(Boolean);

  return { cv, dropped };
}

/** 'generatedCV.experience.0.company' → 'Experience #1 → company'. */
function describeValidationPath(path: string): string {
  const parts = path.replace(/^generatedCV\./, '').split('.');
  const label = parts[0].replace(/([A-Z])/g, ' $1');
  const section = label.charAt(0).toUpperCase() + label.slice(1);
  if (parts.length >= 3 && /^\d+$/.test(parts[1])) {
    return `${section} #${Number(parts[1]) + 1} → ${parts[2]}`;
  }
  return parts.length > 1 ? `${section} → ${parts.slice(1).join(' → ')}` : section;
}

/**
 * Turn a Mongoose ValidationError into a message naming the fields that were
 * actually incomplete. Deliberately avoids the words "validation failed", which
 * the frontend maps to the generic "Missing required information" banner.
 */
export function describeCvValidationError(err: any): string | null {
  if (!err || err.name !== 'ValidationError' || !err.errors) return null;
  const fields = Object.keys(err.errors).map(describeValidationPath);
  const unique = Array.from(new Set(fields));
  return `The generated CV was incomplete — these fields were missing: ${unique.join(', ')}. Please generate again.`;
}
