/**
 * cli:scaffold-entity — generate.ts
 *
 * Generates a Domain entity + its EF Core Configuration. Both consume SmartStack
 * NuGet packages:
 *   - SmartStack.Core.Domain.BaseEntity (provides Id, CreatedAt, UpdatedAt, DeletedAt,
 *     AddDomainEvent, etc.)
 *   - SmartStack.Core.Domain.ITenantEntity / IOptionalTenantEntity
 *   - SmartStack.Infrastructure.Persistence.SchemaConstants (Core / Extensions)
 *
 * Conventions enforced:
 *   - Private parameterless ctor + static Create() factory (no public setters)
 *   - Update() validates required string fields
 *   - SoftDelete()/Restore() methods (DeletedAt-based)
 *   - Table = "{domainPrefix}_{PluralName}" in schemaTarget ('core' for SmartStack
 *     built-ins, 'extensions' for client modules)
 *   - Indexes on CreatedAt (common query filter) and any field flagged `indexed: true`
 */
import { pluralize } from '../../../../../lib/string-utils.js'
import type { ScaffoldEntityInput, GeneratedFile, EntityField, EntityRelation } from './types.js'

export function generate(spec: ScaffoldEntityInput): GeneratedFile[] {
  const ns = spec.namespace ?? spec.appCode
  const reserved = new Set(['Id', 'CreatedAt', 'UpdatedAt', 'DeletedAt', 'TenantId'])
  // Computed fields (with `formula`) are NOT emitted as Domain properties — they
  // live only in the read DTOs where scaffold-business injects the formula into
  // the LINQ projection. EF Configuration also ignores them (no column mapping).
  const userFields = spec.fields.filter(f => !reserved.has(f.name) && !f.formula)

  // ─── Relations → FK columns + navigation properties ───
  // Owning side (many-to-one / one-to-one) carries the FK column on THIS entity;
  // the collection side (one-to-many / many-to-many) only gets an inverse nav.
  const owningRels = spec.relations.filter(r => r.type === 'many-to-one' || r.type === 'one-to-one')
  const collectionRels = spec.relations.filter(r => r.type === 'one-to-many' || r.type === 'many-to-many')
  const present = new Set(userFields.map(f => f.name.toLowerCase()))
  // Synthesize a Guid FK column per owning relation so the Create/Update/column
  // logic treats it like any other field. Skip when the spec already declares the
  // Guid field by hand (cross-module style) — reuse it, only add the config.
  // FK columns are NOT flagged indexed: EF Core auto-creates the FK index.
  const fkFields: EntityField[] = []
  for (const rel of owningRels) {
    const fk = fkName(rel)
    if (present.has(fk.toLowerCase())) continue
    fkFields.push({ name: fk, type: 'guid', required: !rel.nullable, isKey: false, indexed: false })
    present.add(fk.toLowerCase())
  }
  const columns = [...userFields, ...fkFields]
  const reqFields = columns.filter(f => f.required)
  const updatableFields = columns.filter(f => !f.isKey)

  // Navigation properties (reference for the owning side, collection for the inverse).
  const navProps = [
    ...owningRels.map(rel => `    public ${rel.targetEntity}${rel.nullable ? '?' : ''} ${refNavName(rel)} { get; private set; }${rel.nullable ? '' : ' = null!;'}`),
    ...collectionRels.map(rel => `    public ICollection<${rel.targetEntity}> ${collNavName(rel)} { get; private set; } = new List<${rel.targetEntity}>();`),
  ]
  const navBlock = navProps.length ? '\n\n' + navProps.join('\n') : ''

  // EF Core relationship configuration — the REAL FK constraint + declared cascade.
  const relConfig = [
    ...owningRels.map(rel => rel.type === 'one-to-one'
      ? `        builder.HasOne(e => e.${refNavName(rel)}).WithOne().HasForeignKey<${spec.name}>(e => e.${fkName(rel)}).OnDelete(DeleteBehavior.${onDeleteBehavior(rel)});`
      : `        builder.HasOne(e => e.${refNavName(rel)}).WithMany().HasForeignKey(e => e.${fkName(rel)}).OnDelete(DeleteBehavior.${onDeleteBehavior(rel)});`),
    ...collectionRels.map(rel => rel.type === 'many-to-many'
      ? `        builder.HasMany(e => e.${collNavName(rel)}).WithMany();`
      : `        builder.HasMany(e => e.${collNavName(rel)}).WithOne().OnDelete(DeleteBehavior.${onDeleteBehavior(rel)});`),
  ]
  const relBlock = relConfig.length ? '\n\n' + relConfig.join('\n') : ''
  const tenantIface = spec.tenantMode === 'strict' ? ', ITenantEntity' : spec.tenantMode === 'optional' ? ', IOptionalTenantEntity' : ''
  const tenantProp = spec.tenantMode === 'strict'
    ? '\n    public Guid TenantId { get; private set; }'
    : spec.tenantMode === 'optional'
    ? '\n    public Guid? TenantId { get; private set; }'
    : ''
  const schemaConstant = spec.schemaTarget === 'core' ? 'SchemaConstants.Core' : 'SchemaConstants.Extensions'

  const entity = `using SmartStack.Core.Domain;

namespace ${ns}.Domain.Entities;

public class ${spec.name} : BaseEntity${tenantIface}
{${tenantProp}
${columns.map(f => `    public ${csType(f)} ${f.name} { get; private set; }${defaultInitializer(f)}`).join('\n')}${navBlock}

    private ${spec.name}() { }

    public static ${spec.name} Create(${createParams(reqFields, spec.tenantMode !== 'none')})
    {
${reqFields.filter(f => f.type === 'string').map(f => `        ArgumentException.ThrowIfNullOrWhiteSpace(${camel(f.name)});`).join('\n')}
        var entity = new ${spec.name}
        {
${reqFields.map(f => `            ${f.name} = ${camel(f.name)},`).join('\n')}${spec.tenantMode !== 'none' ? '\n            TenantId = tenantId,' : ''}
        };
        entity.AddDomainEvent(new ${spec.name}CreatedEvent(entity.Id));
        return entity;
    }

    public void Update(${updateParams(updatableFields)})
    {
${updatableFields.filter(f => f.type === 'string' && f.required).map(f => `        ArgumentException.ThrowIfNullOrWhiteSpace(${camel(f.name)});`).join('\n')}
${updatableFields.map(f => `        ${f.name} = ${camel(f.name)};`).join('\n')}
        UpdatedAt = DateTime.UtcNow;
        AddDomainEvent(new ${spec.name}UpdatedEvent(Id));
    }

    public void SoftDelete()
    {
        if (DeletedAt.HasValue) return;
        DeletedAt = DateTime.UtcNow;
        AddDomainEvent(new ${spec.name}DeletedEvent(Id));
    }

    public void Restore()
    {
        if (!DeletedAt.HasValue) return;
        DeletedAt = null;
        AddDomainEvent(new ${spec.name}RestoredEvent(Id));
    }
}
`

  const events = `using SmartStack.Core.Domain;

namespace ${ns}.Domain.Entities;

public record ${spec.name}CreatedEvent(Guid ${spec.name}Id) : IDomainEvent;
public record ${spec.name}UpdatedEvent(Guid ${spec.name}Id) : IDomainEvent;
public record ${spec.name}DeletedEvent(Guid ${spec.name}Id) : IDomainEvent;
public record ${spec.name}RestoredEvent(Guid ${spec.name}Id) : IDomainEvent;
`

  const tableName = `${spec.domainPrefix}_${spec.pluralName ?? pluralize(spec.name)}`
  const indexedFields = columns.filter(f => f.indexed)
  // Column constraints only — NO HasColumnName. EF Core maps property → column in
  // PascalCase by default, matching SmartStack.app (which never remaps columns:
  // `FirstName`, `CreatedAt`, `ClientId`). A Property() line is emitted only when
  // it carries a constraint (required string / maxLength); otherwise the column
  // falls through to the default mapping.
  const propLines = columns
    .map(f => {
      const required = f.required && f.type === 'string' ? '.IsRequired()' : ''
      const maxLen = f.maxLength ? `.HasMaxLength(${f.maxLength})` : ''
      return required || maxLen ? `        builder.Property(e => e.${f.name})${required}${maxLen};` : ''
    })
    .filter(Boolean)
  const bodyLines: string[] = [
    `        builder.ToTable("${tableName}", ${schemaConstant});`,
    `        builder.HasKey(e => e.Id);`,
    `        builder.HasQueryFilter(e => e.DeletedAt == null);`,
    ``,
    `        builder.HasIndex(e => e.CreatedAt);`,
  ]
  if (spec.tenantMode !== 'none') bodyLines.push(`        builder.HasIndex(e => e.TenantId);`)
  if (propLines.length) bodyLines.push(``, ...propLines)
  if (indexedFields.length) bodyLines.push(``, ...indexedFields.map(f => `        builder.HasIndex(e => e.${f.name});`))
  const config = `using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SmartStack.Infrastructure.Persistence;
using ${ns}.Domain.Entities;

namespace ${ns}.Infrastructure.Persistence.Configurations;

public class ${spec.name}Configuration : IEntityTypeConfiguration<${spec.name}>
{
    public void Configure(EntityTypeBuilder<${spec.name}> builder)
    {
${bodyLines.join('\n')}${relBlock}
    }
}
`

  return [
    { path: `src/${ns}.Domain/Entities/${spec.name}.cs`, content: entity },
    { path: `src/${ns}.Domain/Entities/${spec.name}Events.cs`, content: events },
    { path: `src/${ns}.Infrastructure/Persistence/Configurations/${spec.name}Configuration.cs`, content: config },
  ]
}

// ─── Helpers ───

function camel(s: string): string {
  return s.charAt(0).toLowerCase() + s.slice(1)
}

function csType(f: EntityField): string {
  const map: Record<string, string> = {
    string: 'string', int: 'int', integer: 'int', number: 'decimal', decimal: 'decimal',
    bool: 'bool', boolean: 'bool', datetime: 'DateTime', date: 'DateOnly', guid: 'Guid',
  }
  const base = map[f.type.toLowerCase()] ?? f.type
  return !f.required && base !== 'string' ? `${base}?` : base
}

function defaultInitializer(f: EntityField): string {
  if (f.required && f.type.toLowerCase() === 'string') return ' = null!;'
  return ''
}

function createParams(reqFields: EntityField[], includeTenant: boolean): string {
  const parts = reqFields.map(f => `${csType(f)} ${camel(f.name)}`)
  if (includeTenant) parts.push('Guid tenantId')
  return parts.join(', ')
}

function updateParams(fields: EntityField[]): string {
  return fields.map(f => `${csType(f)} ${camel(f.name)}`).join(', ')
}

// ─── Relation helpers ───

/** FK column name on the owning side. Default `${targetEntity}Id`. */
function fkName(rel: EntityRelation): string {
  return rel.foreignKey ?? `${rel.targetEntity}Id`
}

/** Reference navigation property name (owning side). Default = targetEntity. */
function refNavName(rel: EntityRelation): string {
  return rel.navigationName ?? rel.targetEntity
}

/** Collection navigation property name (inverse side). Default = `${targetEntity}s`. */
function collNavName(rel: EntityRelation): string {
  return rel.navigationName ?? `${rel.targetEntity}s`
}

/** Maps the BA cascade vocabulary to an EF Core `DeleteBehavior` member. */
function onDeleteBehavior(rel: EntityRelation): string {
  const action = rel.onDelete ?? (rel.cascadeDelete ? 'cascade' : 'restrict')
  const map: Record<string, string> = {
    'restrict': 'Restrict', 'cascade': 'Cascade', 'set-null': 'SetNull', 'no-action': 'NoAction',
  }
  return map[action] ?? 'Restrict'
}
