using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

namespace ZB.MOM.WW.ScadaBridge.Transport.Serialization;

/// <summary>
/// In-memory aggregate of all bundle-eligible Commons entities. Matches the
/// shape of <see cref="BundleContentDto"/> but uses the real persistence-
/// ignorant POCO types — what <see cref="EntitySerializer"/> consumes/produces
/// on the application side of the bundle boundary.
/// </summary>
// ApiKeys is intentionally absent: inbound API keys are not transported between
// environments. The aggregate only carries API methods.
public sealed record EntityAggregate(
    IReadOnlyList<TemplateFolder> TemplateFolders,
    IReadOnlyList<Template> Templates,
    IReadOnlyList<SharedScript> SharedScripts,
    IReadOnlyList<ExternalSystemDefinition> ExternalSystems,
    IReadOnlyList<ExternalSystemMethod> ExternalSystemMethods,
    IReadOnlyList<DatabaseConnectionDefinition> DatabaseConnections,
    IReadOnlyList<NotificationList> NotificationLists,
    IReadOnlyList<SmtpConfiguration> SmtpConfigurations,
    IReadOnlyList<ApiMethod> ApiMethods)
{
    // Site/instance-scoped entities. Init-only with empty-array defaults so
    // existing positional `new EntityAggregate(...)` callers keep compiling and
    // never see a null collection; new callers opt in via object-initializer.
    /// <summary>
    /// The sites carried in this aggregate.
    /// </summary>
    public IReadOnlyList<Site> Sites { get; init; } = Array.Empty<Site>();

    /// <summary>
    /// The site-scoped data connections carried in this aggregate.
    /// </summary>
    public IReadOnlyList<DataConnection> DataConnections { get; init; } = Array.Empty<DataConnection>();

    /// <summary>
    /// The instances carried in this aggregate.
    /// </summary>
    public IReadOnlyList<Instance> Instances { get; init; } = Array.Empty<Instance>();

    // SMS: carried alongside SmtpConfigurations. Init-only with an empty
    // default for the same source-compat reason as the site/instance collections above —
    // every existing positional `new EntityAggregate(...)` caller keeps compiling
    // and never sees null; producers that resolve SMS config opt in via
    // object-initializer.
    /// <summary>
    /// The SMS provider configurations carried in this aggregate.
    /// </summary>
    public IReadOnlyList<SmsConfiguration> SmsConfigurations { get; init; } = Array.Empty<SmsConfiguration>();

    // Area id → name lookup for the exported instances. Areas are not carried as a
    // first-class bundle collection (they reconcile by NAME under the target site),
    // so the aggregate only needs enough to resolve each instance's AreaId FK back to
    // a portable name at serialization time. Init-only with an empty default: producers
    // that export instances populate it; every other caller keeps compiling and the
    // serializer emits AreaName: null when an instance's AreaId isn't in the map.
    /// <summary>
    /// Lookup from area ID to area name for the exported instances' <c>AreaId</c> FKs.
    /// </summary>
    public IReadOnlyDictionary<int, string> AreaNameById { get; init; } =
        new Dictionary<int, string>();
}

/// <summary>
/// Top-level serializable bundle payload. Lists are sequenced in dependency
/// order so importers can apply them inline. Lists are never null on the wire
/// — empty arrays are preferred over nulls so JSON consumers can rely on each
/// property being present — except <see cref="ApiKeys"/>, which is intentionally
/// null-defaulted (see below).
/// <para>
/// <see cref="ApiKeys"/> is a <b>legacy, read-only</b> field retained purely for
/// backward-compatible deserialization of bundles produced before the
/// inbound-API-key re-architecture. New exports never populate it (it stays
/// <c>null</c> and is dropped from the JSON by the serializer's
/// <c>WhenWritingNull</c> policy); the importer counts any keys present in an old
/// bundle, ignores them, and surfaces a note — keys are re-created per environment.
/// </para>
/// </summary>
public sealed record BundleContentDto(
    IReadOnlyList<TemplateFolderDto> TemplateFolders,
    IReadOnlyList<TemplateDto> Templates,
    IReadOnlyList<SharedScriptDto> SharedScripts,
    IReadOnlyList<ExternalSystemDto> ExternalSystems,
    IReadOnlyList<DatabaseConnectionDto> DatabaseConnections,
    IReadOnlyList<NotificationListDto> NotificationLists,
    IReadOnlyList<SmtpConfigDto> SmtpConfigs,
    IReadOnlyList<ApiMethodDto> ApiMethods,
    IReadOnlyList<ApiKeyDto>? ApiKeys = null)
{
    // Site/instance-scoped payloads. Modeled as init-only properties with
    // empty-array defaults (rather than additional positional ctor params) for
    // two reasons:
    //   1. Forward-compat: deserializing an OLDER bundle whose content blob has
    //      no sites/dataConnections/instances fields leaves each property at its
    //      Array.Empty<>() initializer — so consumers always see an empty list,
    //      never null. (Contrast ApiKeys, which is a nullable legacy field.)
    //   2. Source-compat: every existing `new BundleContentDto(...)` positional
    //      caller keeps compiling unchanged; new producers opt in via an
    //      object-initializer (`new BundleContentDto(...) { Sites = ... }`).
    // These are written by the serializer (WhenWritingNull does not apply — they
    // are non-null), so new bundles always carry the three arrays explicitly.
    /// <summary>
    /// The sites carried in this bundle payload.
    /// </summary>
    public IReadOnlyList<SiteDto> Sites { get; init; } = Array.Empty<SiteDto>();

    /// <summary>
    /// The site-scoped data connections carried in this bundle payload.
    /// </summary>
    public IReadOnlyList<DataConnectionDto> DataConnections { get; init; } = Array.Empty<DataConnectionDto>();

    /// <summary>
    /// The instances carried in this bundle payload.
    /// </summary>
    public IReadOnlyList<InstanceDto> Instances { get; init; } = Array.Empty<InstanceDto>();

    // SMS: central-only SMS provider configs, carried alongside SmtpConfigs.
    // Modeled as an init-only property with an empty default (NOT a positional ctor
    // param) for the same two reasons as the site/instance arrays above:
    //   1. Forward-compat: deserializing a bundle written before SMS support leaves
    //      this at Array.Empty<SmsConfigDto>() — consumers always see an empty list,
    //      never null.
    //   2. Source-compat: every existing positional `new BundleContentDto(...)` caller
    //      keeps compiling; producers that pack SMS configs opt in via the
    //      object-initializer.
    /// <summary>
    /// The SMS provider configurations carried in this bundle payload.
    /// </summary>
    public IReadOnlyList<SmsConfigDto> SmsConfigs { get; init; } = Array.Empty<SmsConfigDto>();
}

/// <summary>
/// Carved-off secret values for an entity. The outer DTO carries all non-
/// sensitive fields; secrets land here so a future "share without secrets"
/// export mode can drop this block without touching anything else.
/// </summary>
public sealed record SecretsBlock(IReadOnlyDictionary<string, string> Values);

public sealed record TemplateFolderDto(
    string Name,
    string? ParentName,
    int SortOrder);

public sealed record TemplateDto(
    string Name,
    string? FolderName,
    string? BaseTemplateName,
    string? Description,
    IReadOnlyList<TemplateAttributeDto> Attributes,
    IReadOnlyList<TemplateAlarmDto> Alarms,
    IReadOnlyList<TemplateScriptDto> Scripts,
    IReadOnlyList<TemplateCompositionDto> Compositions)
{
    // Template-defined native alarm source bindings. Modeled as an init-only
    // property with an empty default (NOT a positional ctor param) because an
    // IReadOnlyList has no valid compile-time default for a positional
    // parameter, and for the same forward/source-compat reasons as the
    // site/instance arrays on BundleContentDto:
    //   1. Forward-compat: a bundle written before native alarm sources were
    //      transported has no NativeAlarmSources field and deserializes this to
    //      Array.Empty<> — consumers always see an empty list, never null.
    //   2. Source-compat: every existing positional `new TemplateDto(...)` caller
    //      keeps compiling; producers opt in via the object-initializer.
    // IsInherited placeholder rows ARE carried (the collision detector depends
    // on them), mirroring how the flattener treats inherited native sources.
    /// <summary>
    /// The template-defined native alarm source bindings carried for this template.
    /// </summary>
    public IReadOnlyList<TemplateNativeAlarmSourceDto> NativeAlarmSources { get; init; } =
        Array.Empty<TemplateNativeAlarmSourceDto>();
}

/// <summary>
/// A template-defined native alarm source binding. Carries every writable field
/// of the <c>TemplateNativeAlarmSource</c> entity, including the inheritance/lock
/// flags, so an export→import round-trip is field-faithful and instance-level
/// overrides of the source no longer dangle in the target environment.
/// </summary>
public sealed record TemplateNativeAlarmSourceDto(
    string Name,
    string? Description,
    string ConnectionName,
    string SourceReference,
    string? ConditionFilter,
    bool IsLocked,
    bool IsInherited,
    bool LockedInDerived);

public sealed record TemplateAttributeDto(
    string Name,
    string? Value,
    DataType DataType,
    bool IsLocked,
    string? Description,
    string? DataSourceReference,
    DataType? ElementDataType = null,
    // Whether this member is locked against override in derived templates.
    // Additive trailing field; false on bundles written before it existed
    // (dropped from JSON by WhenWritingNull? — no, bool is a value type, so it
    // is always emitted; older bundles simply lack the property and deserialize
    // to the default false).
    bool LockedInDerived = false);

public sealed record TemplateAlarmDto(
    string Name,
    string? Description,
    int PriorityLevel,
    AlarmTriggerType TriggerType,
    string? TriggerConfiguration,
    bool IsLocked,
    string? OnTriggerScriptName,
    // Derived-lock flag; additive trailing field (see TemplateAttributeDto).
    bool LockedInDerived = false);

public sealed record TemplateScriptDto(
    string Name,
    string Code,
    string? TriggerType,
    string? TriggerConfiguration,
    string? ParameterDefinitions,
    string? ReturnDefinition,
    bool IsLocked,
    TimeSpan? MinTimeBetweenRuns,
    // Per-script execution timeout (seconds). Additive trailing field;
    // null on bundles written before this field existed.
    int? ExecutionTimeoutSeconds = null,
    // Derived-lock flag; additive trailing field (see TemplateAttributeDto).
    bool LockedInDerived = false);

public sealed record TemplateCompositionDto(
    string InstanceName,
    string ComposedTemplateName);

public sealed record SharedScriptDto(
    string Name,
    string Code,
    string? ParameterDefinitions,
    string? ReturnDefinition);

public sealed record ExternalSystemDto(
    string Name,
    string BaseUrl,
    string AuthType,
    int MaxRetries,
    TimeSpan RetryDelay,
    IReadOnlyList<ExternalSystemMethodDto> Methods,
    SecretsBlock? Secrets);

public sealed record ExternalSystemMethodDto(
    string Name,
    string HttpMethod,
    string Path,
    string? ParameterDefinitions,
    string? ReturnDefinition);

public sealed record DatabaseConnectionDto(
    string Name,
    int MaxRetries,
    TimeSpan RetryDelay,
    SecretsBlock? Secrets);

public sealed record NotificationListDto(
    string Name,
    NotificationType Type,
    IReadOnlyList<NotificationRecipientDto> Recipients);

public sealed record NotificationRecipientDto(
    string Name,
    // Nullable so an SMS-only recipient (no email) round-trips faithfully
    // instead of being coerced to an empty string. Email-only recipients (the
    // historical case) still carry a non-null address; the entity enforces that at
    // least one contact is present.
    string? EmailAddress,
    // SMS recipients carry an E.164 phone number instead of (or in addition
    // to) an email. Trailing + nullable so a bundle written before SMS support
    // deserializes this as null (backward-compatible); the serializer's
    // WhenWritingNull policy keeps it out of email-only bundles' JSON.
    string? PhoneNumber = null);

/// <summary>
/// An email delivery configuration (the <c>SmtpConfiguration</c> entity). Every
/// non-sensitive field is carried directly and only the credential (SMTP password
/// or OAuth2 client secret) rides inside <see cref="Secrets"/>, so a future
/// "share without secrets" export can drop it as a unit.
/// </summary>
public sealed record SmtpConfigDto(
    string Host,
    int Port,
    string AuthType,
    string FromAddress,
    string? TlsMode,
    int ConnectionTimeoutSeconds,
    int MaxConcurrentConnections,
    int MaxRetries,
    TimeSpan RetryDelay,
    SecretsBlock? Secrets,
    // OAuth2 token-endpoint authority and scope: endpoint URLs, not secrets, so
    // they belong on the public DTO rather than in the SecretsBlock. Trailing +
    // nullable so a bundle written before these were carried deserializes them as
    // null (= provider defaults); WhenWritingNull keeps them out of the JSON of
    // configurations that don't set them.
    string? OAuth2Authority = null,
    string? OAuth2Scope = null,
    // Delivery transport ("Smtp" or "Ews"); null means Smtp. Trailing + nullable
    // for the same additive-evolution reason — no bundleFormatVersion bump.
    string? Transport = null);

/// <summary>
/// An SMS provider configuration (the <c>SmsConfiguration</c> entity). Mirrors
/// <see cref="SmtpConfigDto"/>: all non-sensitive fields are carried directly and
/// the provider auth token rides inside <see cref="Secrets"/> so a future
/// "share without secrets" export can drop it as a unit.
/// </summary>
public sealed record SmsConfigDto(
    string AccountSid,
    string? FromNumber,
    string? MessagingServiceSid,
    string? ApiBaseUrl,
    int ConnectionTimeoutSeconds,
    int MaxRetries,
    TimeSpan RetryDelay,
    SecretsBlock? Secrets);

// Legacy DTO: only deserialized from bundles produced before the inbound-API-key
// re-architecture so the importer can count and ignore the keys they contain.
// New exports never emit an ApiKeys array.
public sealed record ApiKeyDto(
    string Name,
    string KeyHash,
    bool IsEnabled,
    SecretsBlock? Secrets);

// ApprovedApiKeyIds is intentionally absent: it linked methods to keys that are no
// longer transported. Scopes are re-granted per environment. The field
// in any old bundle is simply ignored on read.
public sealed record ApiMethodDto(
    string Name,
    string Script,
    string? ParameterDefinitions,
    string? ReturnDefinition,
    int TimeoutSeconds);

// --- Site/instance-scoped transport DTOs --------------------------------
// These travel alongside the original central-config DTOs above. Sites and
// instances are referenced by their stable string identities (SiteIdentifier,
// UniqueName, TemplateName) rather than database ids so a bundle resolves
// correctly against the *target* environment's own surrogate keys.

/// <summary>
/// A site definition. Addresses are carried verbatim; the importer decides
/// whether to keep or rewrite them for the target environment.
/// </summary>
public sealed record SiteDto(
    string SiteIdentifier,
    string Name,
    string? Description,
    string? NodeAAddress,
    string? NodeBAddress,
    string? GrpcNodeAAddress,
    string? GrpcNodeBAddress);

/// <summary>
/// A site-scoped protocol connection (the <c>Sites.DataConnection</c> entity —
/// NOT the External-System <see cref="DatabaseConnectionDto"/>). The protocol-
/// specific Primary/Backup configuration JSON rides inside <see cref="Secrets"/>
/// so a "share without secrets" export can drop it as a unit.
/// </summary>
public sealed record DataConnectionDto(
    string SiteIdentifier,
    string Name,
    string Protocol,
    int FailoverRetryCount,
    SecretsBlock? Secrets);

public sealed record InstanceAttributeOverrideDto(
    string AttributeName,
    string? OverrideValue,
    DataType? ElementDataType);

public sealed record InstanceAlarmOverrideDto(
    string AlarmCanonicalName,
    string? TriggerConfigurationOverride,
    int? PriorityLevelOverride);

public sealed record InstanceNativeAlarmSourceOverrideDto(
    string SourceCanonicalName,
    string? ConnectionNameOverride,
    string? SourceReferenceOverride,
    string? ConditionFilterOverride);

public sealed record InstanceConnectionBindingDto(
    string AttributeName,
    string ConnectionName,
    string? DataSourceReferenceOverride);

/// <summary>
/// A deployable instance with all of its per-instance overrides and bindings.
/// References its template and site by name/identifier (not id).
/// </summary>
public sealed record InstanceDto(
    string UniqueName,
    string TemplateName,
    string SiteIdentifier,
    string? AreaName,
    InstanceState State,
    IReadOnlyList<InstanceAttributeOverrideDto> AttributeOverrides,
    IReadOnlyList<InstanceAlarmOverrideDto> AlarmOverrides,
    IReadOnlyList<InstanceNativeAlarmSourceOverrideDto> NativeAlarmSourceOverrides,
    IReadOnlyList<InstanceConnectionBindingDto> ConnectionBindings);
