/**
 * cli:scaffold-seed — generate.ts
 * Generates IClientSeedDataProvider implementation.
 *
 * Consumes SmartStack NuGet packages:
 *  - SmartStack.Application.Common.Interfaces.Seeding.IClientSeedDataProvider
 *  - SmartStack.Core.Domain.Navigation (NavigationApplication/Module/Section/Resource entities)
 *  - SmartStack.Core.Domain.Auth (Role, NavigationPermission, RolePermission)
 *
 * Idempotence rules (SmartStack convention):
 *  - `Order` stable across runs
 *  - GUIDs declared as `static readonly` once per provider instance; Guid.NewGuid()
 *    is only called the first time the class is loaded. Re-running seed does NOT
 *    regenerate GUIDs.
 *  - Each `Seed*Async` checks existence before inserting (composite key when needed).
 *  - Every navigation entry carries a `ComponentKey` aligned with the frontend
 *    PageRegistry key (dotted form: {appCode}.{module}[.{section}[.{resource}]]).
 *
 * When `testUsers[]` is non-empty, additionally generates:
 *  - `{Module}TestUserSeedDataProvider.cs` — gated by IHostEnvironment.IsDevelopment()
 *  - `tests/ui-test/test-users.json` — manifest consumed by Phase 5 ui-test runner
 */
import type {
  ScaffoldSeedInput,
  GeneratedFile,
  NavigationEntry,
  NavigationLevel,
  TestUserEntry,
  RoleEntry,
} from './types.js'
import { toPascalCase } from '../../../../../lib/string-utils.js'

export function generate(spec: ScaffoldSeedInput): GeneratedFile[] {
  // Multi-app layout (post core-seed Variante B): module-scoped providers live
  // under Persistence/Seeding/Applications/{AppPascal}/Modules/{ModuleCode}/.
  // The Seeding/Applications/{AppPascal}/Core/ siblings are owned by the 5
  // {AppPascal}Core* providers emitted by scaffold-core-seed (Phase 0).
  const appPascal = toPascalCase(spec.applicationCode)
  const moduleFolder = capitalize(spec.module)
  const ns = spec.namespace ?? `${spec.appCode}.Infrastructure.Persistence.Seeding.Applications.${appPascal}.Modules.${moduleFolder}`
  const className = `${capitalize(spec.module)}SeedDataProvider`
  const byLevel = (level: NavigationLevel) => spec.navigation.filter(n => n.level === level)
  const apps = byLevel('application')
  const modules = byLevel('module')
  const sections = byLevel('section')
  const resources = byLevel('resource')
  const navByCode = new Map(spec.navigation.map(n => [`${n.level}:${n.code}`, n]))

  const content = `using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Core.Domain.Navigation;
using SmartStack.Core.Domain.Auth;

namespace ${ns};

/// <summary>
/// Seeds navigation, roles, permissions, and role-permission mappings
/// for the ${capitalize(spec.module)} module into SmartStack core tables.
/// Implementation is idempotent: safe to run multiple times.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    public int Order => ${spec.order};

    // ─── Static GUIDs (one per instance, stable across Seed* calls) ───
${generateGuidFields(spec.navigation)}
${spec.roles.map(r => `    private static readonly Guid ${capitalize(r.code)}RoleId = Guid.NewGuid();`).join('\n')}

    public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${generateApplicationSeed(apps, spec.appCode)}

${generateModuleSeed(modules, spec.appCode, navByCode)}

${generateSectionSeed(sections, spec.appCode, navByCode)}

${generateResourceSeed(resources, spec.appCode, navByCode)}

        await context.SaveChangesAsync(ct);
    }

    public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${spec.roles.length === 0 ? '        // No custom roles defined' : spec.roles.map(r => `
        if (!await context.Roles.AnyAsync(x => x.Code == "${r.code}" && x.ApplicationId == ${capitalize(r.applicationCode)}ApplicationId, ct))
        {
            var role = Role.Create(
                id: ${capitalize(r.code)}RoleId,
                code: "${r.code}",
                name: "${escapeString(r.name)}",
                applicationId: ${capitalize(r.applicationCode)}ApplicationId,
                isDefault: ${r.isDefault});
            context.Roles.Add(role);
        }`).join('\n')}

        await context.SaveChangesAsync(ct);
    }

    public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${spec.permissions.length === 0 ? '        // No custom permissions defined' : `        var permissionsToSeed = new (string Path, string Action, string SectionCode)[]
        {
${spec.permissions.map(p => `            ("${p.path}", "${p.action}", "${p.sectionCode}"),`).join('\n')}
        };

        foreach (var perm in permissionsToSeed)
        {
            if (!await context.NavigationPermissions.AnyAsync(
                x => x.Path == perm.Path && x.Action == perm.Action && x.SectionCode == perm.SectionCode,
                ct))
            {
                var permission = NavigationPermission.Create(
                    id: Guid.NewGuid(),
                    path: perm.Path,
                    action: perm.Action,
                    sectionCode: perm.SectionCode);
                context.NavigationPermissions.Add(permission);
            }
        }

        await context.SaveChangesAsync(ct);`}
    }

    public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
${spec.rolePermissions.length === 0 ? '        // No role-permission mappings defined' : `        var mappings = new (string RoleCode, string PermissionPath)[]
        {
${spec.rolePermissions.map(rp => `            ("${rp.roleCode}", "${rp.permissionPath}"),`).join('\n')}
        };

        foreach (var mapping in mappings)
        {
            var role = await context.Roles.FirstOrDefaultAsync(r => r.Code == mapping.RoleCode, ct);
            var permission = await context.NavigationPermissions.FirstOrDefaultAsync(
                p => p.Path == mapping.PermissionPath,
                ct);

            if (role is null || permission is null) continue;

            if (!await context.RolePermissions.AnyAsync(
                rp => rp.RoleId == role.Id && rp.PermissionId == permission.Id,
                ct))
            {
                context.RolePermissions.Add(RolePermission.Create(role.Id, permission.Id));
            }
        }

        await context.SaveChangesAsync(ct);`}
    }
}
`

  const seedingDir = `src/${spec.appCode}.Infrastructure/Persistence/Seeding/Applications/${appPascal}/Modules/${moduleFolder}`
  const files: GeneratedFile[] = [
    { path: `${seedingDir}/${className}.cs`, content },
  ]

  if (spec.testUsers.length > 0) {
    const testUserClassName = `${capitalize(spec.module)}TestUserSeedDataProvider`
    files.push({
      path: `${seedingDir}/${testUserClassName}.cs`,
      content: generateTestUserProvider(spec, testUserClassName, ns),
    })

    if (spec.emitDevCredentialsManifest) {
      files.push({
        path: 'tests/ui-test/test-users.json',
        content: generateTestUsersManifest(spec),
      })
    }
  }

  return files
}

// ─── Helpers ───

function capitalize(s: string): string {
  return s.charAt(0).toUpperCase() + s.slice(1)
}

function escapeString(s: string): string {
  return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

function emailFor(tu: TestUserEntry, domain: string): string {
  const local = tu.emailLocalPart ?? tu.roleCode
  return `${local}.test@${domain}`
}

function guidVar(entry: NavigationEntry): string {
  const levelSuffix = entry.level === 'application' ? 'ApplicationId'
    : entry.level === 'module' ? 'ModuleId'
    : entry.level === 'section' ? 'SectionId'
    : 'ResourceId'
  return `${capitalize(entry.code)}${levelSuffix}`
}

function componentKeyFor(entry: NavigationEntry, appCode: string, navByCode: Map<string, NavigationEntry>): string {
  if (entry.componentKey) return entry.componentKey

  const segments: string[] = []
  let current: NavigationEntry | undefined = entry
  while (current) {
    segments.unshift(current.code)
    if (!current.parentCode) break
    const parentLevel = parentLevelOf(current.level)
    if (!parentLevel) break
    current = navByCode.get(`${parentLevel}:${current.parentCode}`)
  }

  // Navigation codes for 'application' level start at appCode root; for other
  // levels the chain above already walked up to the application, so prepend
  // appCode only if it's not already the head.
  if (segments[0] !== appCode) {
    segments.unshift(appCode)
  }

  return segments.join('.')
}

function parentLevelOf(level: NavigationLevel): NavigationLevel | null {
  switch (level) {
    case 'module': return 'application'
    case 'section': return 'module'
    case 'resource': return 'section'
    default: return null
  }
}

function generateGuidFields(navigation: NavigationEntry[]): string {
  return navigation
    .map(n => `    private static readonly Guid ${guidVar(n)} = Guid.NewGuid();`)
    .join('\n')
}

function generateApplicationSeed(apps: NavigationEntry[], appCode: string): string {
  if (apps.length === 0) return `        // No application entries`
  return apps.map(a => `        if (!await context.NavigationApplications.AnyAsync(x => x.Code == "${a.code}", ct))
        {
            var app = NavigationApplication.Create(
                id: ${guidVar(a)},
                code: "${a.code}",
                label: "${escapeString(a.label)}",
                icon: "${a.icon}",
                iconType: IconType.${a.iconType},
                route: "${a.route}",
                displayOrder: ${a.displayOrder},
                componentKey: "${componentKeyFor(a, appCode, new Map([['application:' + a.code, a]]))}");
            context.NavigationApplications.Add(app);
            await context.SaveChangesAsync(ct);
        }`).join('\n\n')
}

function generateModuleSeed(modules: NavigationEntry[], appCode: string, navByCode: Map<string, NavigationEntry>): string {
  if (modules.length === 0) return `        // No module entries`
  return modules.map(m => {
    const parentVar = m.parentCode ? guidVar({ ...m, code: m.parentCode, level: 'application' } as NavigationEntry) : 'null'
    return `        if (!await context.NavigationModules.AnyAsync(x => x.Code == "${m.code}" && x.ApplicationId == ${parentVar}, ct))
        {
            var module = NavigationModule.Create(
                id: ${guidVar(m)},
                code: "${m.code}",
                label: "${escapeString(m.label)}",
                icon: "${m.icon}",
                iconType: IconType.${m.iconType},
                route: "${m.route}",
                displayOrder: ${m.displayOrder},
                applicationId: ${parentVar},
                componentKey: "${componentKeyFor(m, appCode, navByCode)}");
            context.NavigationModules.Add(module);
            await context.SaveChangesAsync(ct);
        }`
  }).join('\n\n')
}

function generateSectionSeed(sections: NavigationEntry[], appCode: string, navByCode: Map<string, NavigationEntry>): string {
  if (sections.length === 0) return `        // No section entries`
  return sections.map(s => {
    const parentVar = s.parentCode ? guidVar({ ...s, code: s.parentCode, level: 'module' } as NavigationEntry) : 'null'
    return `        if (!await context.NavigationSections.AnyAsync(x => x.Code == "${s.code}" && x.ModuleId == ${parentVar}, ct))
        {
            var section = NavigationSection.Create(
                id: ${guidVar(s)},
                code: "${s.code}",
                label: "${escapeString(s.label)}",
                icon: "${s.icon}",
                iconType: IconType.${s.iconType},
                route: "${s.route}",
                displayOrder: ${s.displayOrder},
                moduleId: ${parentVar},
                componentKey: "${componentKeyFor(s, appCode, navByCode)}");
            context.NavigationSections.Add(section);
            await context.SaveChangesAsync(ct);
        }`
  }).join('\n\n')
}

function generateResourceSeed(resources: NavigationEntry[], appCode: string, navByCode: Map<string, NavigationEntry>): string {
  if (resources.length === 0) return `        // No resource entries`
  return resources.map(r => {
    const parentVar = r.parentCode ? guidVar({ ...r, code: r.parentCode, level: 'section' } as NavigationEntry) : 'null'
    return `        if (!await context.NavigationResources.AnyAsync(x => x.Code == "${r.code}" && x.SectionId == ${parentVar}, ct))
        {
            var resource = NavigationResource.Create(
                id: ${guidVar(r)},
                code: "${r.code}",
                label: "${escapeString(r.label)}",
                icon: "${r.icon}",
                iconType: IconType.${r.iconType},
                route: "${r.route}",
                displayOrder: ${r.displayOrder},
                sectionId: ${parentVar},
                componentKey: "${componentKeyFor(r, appCode, navByCode)}");
            context.NavigationResources.Add(resource);
            await context.SaveChangesAsync(ct);
        }`
  }).join('\n\n')
}

// ─── Test users (Phase 5 ui-test) ───

function generateTestUserProvider(spec: ScaffoldSeedInput, className: string, ns: string): string {
  const rolesByCode = new Map<string, RoleEntry>(spec.roles.map(r => [r.code, r]))

  const userInserts = spec.testUsers.map(tu => {
    const role = rolesByCode.get(tu.roleCode)
    if (!role) return `        // Skipped test user: role "${tu.roleCode}" not found in spec`
    const email = emailFor(tu, spec.testUserEmailDomain)
    const localPart = tu.emailLocalPart ?? tu.roleCode
    return `        await EnsureTestUserAsync(context, "${email}", "${escapeString(localPart)} (test)", "${role.code}", ${capitalize(role.code)}RoleId, TestUserPassword, ct);`
  }).join('\n')

  const roleIdFields = Array.from(new Set(spec.testUsers.map(tu => tu.roleCode)))
    .map(code => {
      const role = rolesByCode.get(code)
      if (!role) return ''
      return `    private static readonly Guid ${capitalize(code)}RoleId = ${capitalize(spec.module)}SeedDataProvider.${capitalize(code)}RoleId_Public;`
    }).filter(Boolean).join('\n')

  return `using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using SmartStack.Application.Common.Interfaces.Seeding;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Core.Domain.Auth;

namespace ${ns};

/// <summary>
/// Seeds test users (one per role) for Phase 5 UI tests. Gated by
/// IHostEnvironment.IsDevelopment() — production deployments never run
/// this provider, so the hardcoded password below is dev-only.
///
/// PROJECT-SPECIFIC CUSTOMIZATION REQUIRED:
/// The User entity factory and password hashing API differ per project.
/// Adapt EnsureTestUserAsync below to your User entity. Common patterns:
///   - User.CreateForDevelopment(...)  (factory method)
///   - new User { Email = ..., PasswordHash = passwordHasher.Hash(...) }
///   - IUserAdminService.CreateAsync(...)  (application service)
///
/// Order is set ABOVE the main seeder so navigation/roles exist when this runs.
/// </summary>
public class ${className} : IClientSeedDataProvider
{
    private readonly IHostEnvironment _env;

    public ${className}(IHostEnvironment env)
    {
        _env = env;
    }

    public int Order => ${spec.order + 1};

    // Hardcoded dev password — only used when IsDevelopment() is true.
    private const string TestUserPassword = "${escapeString(spec.testUserPassword)}";

    // Re-resolves the same role GUIDs used by ${className.replace('TestUser', '')} —
    // see the public re-export pattern in the sibling provider class.
${roleIdFields || '    // (no test users with matching roles)'}

    public Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct = default) => Task.CompletedTask;

    public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct = default)
    {
        // Test users live here so they run AFTER role-permission mappings.
        // Do nothing in production — credentials in this file are dev-only.
        if (!_env.IsDevelopment())
        {
            return;
        }

${userInserts || '        // No test users defined'}

        await context.SaveChangesAsync(ct);
    }

    /// <summary>
    /// PROJECT-SPECIFIC: adapt this to your User entity API. The default below
    /// uses a generic pattern that may or may not compile against your codebase
    /// — update the factory call to match your actual User type. Once the file
    /// compiles and runs cleanly, remove this comment.
    /// </summary>
    private static async Task EnsureTestUserAsync(
        ICoreDbContext context,
        string email,
        string displayName,
        string roleCode,
        Guid roleId,
        string password,
        CancellationToken ct)
    {
        if (await context.Users.AnyAsync(u => u.Email == email, ct))
        {
            return;
        }

        // TODO(adapt): replace with your project's User factory + password hashing.
        // Example shapes seen in SmartStack apps:
        //   var user = User.CreateForDevelopment(Guid.NewGuid(), email, displayName, password);
        //   var user = new User { Id = Guid.NewGuid(), Email = email, ... }; user.SetPassword(password);
        var user = User.CreateForDevelopment(
            id: Guid.NewGuid(),
            email: email,
            displayName: displayName,
            password: password);
        context.Users.Add(user);

        // Idempotent role assignment.
        if (!await context.UserRoles.AnyAsync(ur => ur.UserId == user.Id && ur.RoleId == roleId, ct))
        {
            context.UserRoles.Add(UserRole.Create(user.Id, roleId));
        }
    }
}
`
}

function generateTestUsersManifest(spec: ScaffoldSeedInput): string {
  const rolesByCode = new Map<string, RoleEntry>(spec.roles.map(r => [r.code, r]))
  const users = spec.testUsers
    .map(tu => {
      const role = rolesByCode.get(tu.roleCode)
      if (!role) return null
      return {
        role: tu.roleCode,
        email: emailFor(tu, spec.testUserEmailDomain),
        password: spec.testUserPassword,
        applicationCode: role.applicationCode,
        roleName: role.name,
      }
    })
    .filter((u): u is NonNullable<typeof u> => u !== null)

  return JSON.stringify(
    {
      generatedAt: new Date().toISOString(),
      module: spec.module,
      appCode: spec.appCode,
      // Gitignored. Used by skills/development/testing/ui-test/cli/run-ui-test.
      // Regenerated on every scaffold-seed run — do not edit by hand.
      users,
    },
    null,
    2,
  ) + '\n'
}
