/**
 * cli:scaffold-core-seed — validate.ts
 *
 * Two-stage validation:
 *  1. Structural (Zod) — schema parse, regex on codes/paths
 *  2. Business — per-application, every module in `modules[]` MUST have
 *     nav + ≥ 1 role + ≥ 2 permissions. Application codes are unique
 *     across the project. This is the gate the previous per-module
 *     pipeline lacked (Budget had 9 entities + 2 controllers but empty
 *     SeedNavigationAsync).
 */
import {
  CoreSeedSpecSchema,
  type ApplicationSpec,
  type CoreSeedSpec,
  type ValidationResult,
} from './types.js';

export function validateStructure(raw: unknown): ValidationResult {
  const result = CoreSeedSpecSchema.safeParse(raw);
  if (result.success) return { valid: true, errors: [], warnings: [] };
  const errors = result.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`);
  return { valid: false, errors, warnings: [] };
}

export function validateBusinessRules(spec: CoreSeedSpec): ValidationResult {
  const errors: string[] = [];
  const warnings: string[] = [];

  // Project-level: application codes are unique.
  const seen = new Set<string>();
  for (const app of spec.applications) {
    if (seen.has(app.code)) {
      errors.push(`Duplicate application code "${app.code}" in spec.applications[]`);
    }
    seen.add(app.code);
  }

  for (const app of spec.applications) {
    validateApplication(app, errors, warnings);
  }

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

function validateApplication(app: ApplicationSpec, errors: string[], warnings: string[]): void {
  const ctx = `applications[${app.code}]`;

  // 1. Exactly one application-level entry, matching `app.code`.
  const apps = app.navigation.filter((n) => n.level === 'application');
  if (apps.length === 0) {
    errors.push(`${ctx}: navigation must include exactly one application-level entry`);
  } else if (apps.length > 1) {
    errors.push(`${ctx}: navigation contains ${apps.length} application-level entries — only one is allowed per application slice`);
  } else if (apps[0].code !== app.code) {
    errors.push(`${ctx}: application-level navigation code "${apps[0].code}" must equal application.code "${app.code}"`);
  }

  // 2. Every declared module has a matching nav entry.
  const moduleNavCodes = new Set(app.navigation.filter((n) => n.level === 'module').map((n) => n.code));
  const missingModuleNav = app.modules.filter((m) => !moduleNavCodes.has(m));
  if (missingModuleNav.length > 0) {
    errors.push(
      `${ctx}: modules declared but missing from navigation: ${missingModuleNav.join(', ')}. Each module in modules[] must have a level: 'module' navigation entry — otherwise its menu would be empty at runtime.`,
    );
  }

  // 3. Every app has ≥ 1 role scoped to its code.
  const rolesForApp = app.roles.filter((r) => r.applicationCode === app.code);
  if (rolesForApp.length === 0) {
    errors.push(
      `${ctx}: no roles defined with applicationCode "${app.code}". Every Core seed must include at least one role per app (typically one per actor).`,
    );
  }

  // 4. Every declared module has ≥ 2 permissions referencing one of its
  // sections (access + read floor).
  const sectionsByModuleCode = new Map<string, string[]>();
  for (const sect of app.navigation.filter((n) => n.level === 'section')) {
    if (!sect.parentCode) continue;
    const arr = sectionsByModuleCode.get(sect.parentCode) ?? [];
    arr.push(sect.code);
    sectionsByModuleCode.set(sect.parentCode, arr);
  }
  for (const moduleCode of app.modules) {
    const sections = sectionsByModuleCode.get(moduleCode) ?? [];
    const sectionSet = new Set(sections);
    const modulePerms = app.permissions.filter((p) => sectionSet.has(p.sectionCode));
    if (modulePerms.length < 2) {
      errors.push(
        `${ctx}: module "${moduleCode}" has ${modulePerms.length} permission(s) — minimum is 2 (access + read). Sections under this module: [${sections.join(', ') || '(none)'}].`,
      );
    }
  }

  // 5. Permissions reference existing sections.
  const allSectionCodes = new Set(app.navigation.filter((n) => n.level === 'section').map((n) => n.code));
  for (const perm of app.permissions) {
    if (!allSectionCodes.has(perm.sectionCode)) {
      warnings.push(`${ctx}: permission ${perm.path} references unknown section: ${perm.sectionCode}`);
    }
  }

  // 6. Role-permissions reference existing roles + permissions.
  const roleCodes = new Set(app.roles.map((r) => r.code));
  const permPaths = new Set(app.permissions.map((p) => p.path));
  for (const rp of app.rolePermissions) {
    if (!roleCodes.has(rp.roleCode)) {
      warnings.push(`${ctx}: role-permission references unknown role: ${rp.roleCode}`);
    }
    if (!permPaths.has(rp.permissionPath)) {
      warnings.push(`${ctx}: role-permission references unknown permission: ${rp.permissionPath}`);
    }
  }

  // 7. Test users reference existing roles (hard error — Phase 5 needs them
  // to log in, a typo here surfaces as 401s in UI tests).
  for (const tu of app.testUsers) {
    if (!roleCodes.has(tu.roleCode)) {
      errors.push(`${ctx}: test user references unknown role: ${tu.roleCode}. Add the role to applications[${app.code}].roles or fix the typo.`);
    }
  }
}

export function validate(raw: unknown): ValidationResult {
  const structural = validateStructure(raw);
  if (!structural.valid) return structural;
  return validateBusinessRules(raw as CoreSeedSpec);
}
