using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;

namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;

/// <summary>
/// Orchestrates the TemplateEngine services (FlatteningService, ValidationService, RevisionHashService)
/// into a single pipeline for deployment use.
///
/// This captures template state at the time of flatten, ensuring that concurrent template edits
/// (last-write-wins) do not conflict with in-progress deployments.
/// </summary>
public class FlatteningPipeline : IFlatteningPipeline
{
    private readonly ITemplateEngineRepository _templateRepo;
    private readonly ISiteRepository _siteRepo;
    private readonly FlatteningService _flatteningService;
    private readonly ValidationService _validationService;
    private readonly RevisionHashService _revisionHashService;
    private readonly ISharedSchemaRepository _sharedSchemaRepo;
    private readonly ITemplateGraphWatermark _watermark;

    /// <summary>Initializes a new <see cref="FlatteningPipeline"/> with the required template engine and site repositories and services.</summary>
    /// <param name="templateRepo">Repository for loading templates and instance data.</param>
    /// <param name="siteRepo">Repository for loading site data used during validation.</param>
    /// <param name="flatteningService">Service that flattens the template inheritance chain into a resolved config.</param>
    /// <param name="validationService">Service that performs semantic validation on the flattened config.</param>
    /// <param name="revisionHashService">Service that computes the revision hash for staleness detection.</param>
    /// <param name="sharedSchemaRepo">
    /// Repository backing the JSON-Schema <c>$ref</c> resolution seam. Used to
    /// look up <c>lib:Name</c> library references so a dangling reference in any validated
    /// script schema becomes a deploy-blocking error.
    /// </param>
    /// <param name="watermark">
    /// Process-wide template version watermark. Supplies the per-template version
    /// used as the <see cref="FlattenSession"/> cache-key discriminator, so a
    /// memoised chain is dropped the moment any of its templates is edited.
    /// </param>
    public FlatteningPipeline(
        ITemplateEngineRepository templateRepo,
        ISiteRepository siteRepo,
        FlatteningService flatteningService,
        ValidationService validationService,
        RevisionHashService revisionHashService,
        ISharedSchemaRepository sharedSchemaRepo,
        ITemplateGraphWatermark watermark)
    {
        _templateRepo = templateRepo;
        _siteRepo = siteRepo;
        _flatteningService = flatteningService;
        _validationService = validationService;
        _revisionHashService = revisionHashService;
        _sharedSchemaRepo = sharedSchemaRepo;
        _watermark = watermark;
    }

    /// <inheritdoc />
    public FlattenSession CreateSession() => new(_watermark);

    /// <inheritdoc />
    public async Task<Result<FlatteningPipelineResult>> FlattenAndValidateAsync(
        int instanceId,
        CancellationToken cancellationToken = default,
        bool validateScripts = true,
        FlattenSession? session = null)
    {
        // A caller that supplies no session gets a private one. That still pays
        // off inside a single flatten: the composed-chain walk below reaches the
        // same template through several composing parents, and the memo collapses
        // those to one load each.
        session ??= CreateSession();

        // Load instance with full graph
        var instance = await _templateRepo.GetInstanceByIdAsync(instanceId, cancellationToken);
        if (instance == null)
            return Result<FlatteningPipelineResult>.Failure($"Instance with ID {instanceId} not found.");

        Task<Commons.Entities.Templates.Template?> LoadTemplate(int id) =>
            _templateRepo.GetTemplateWithChildrenAsync(id, cancellationToken);

        // Build template chain
        var templateChain = await session.GetChainAsync(instance.TemplateId, LoadTemplate);
        if (templateChain.Count == 0)
            return Result<FlatteningPipelineResult>.Failure("Template chain is empty.");

        // Build composition maps, walking nested compositions so the flattener
        // can resolve composed-of-composed attributes / alarms / scripts (e.g.
        // a parent that composes Pump where Pump itself composes AlarmSensor
        // produces "Pump.AlarmSensor.SensorReading").
        var compositionMap = new Dictionary<int, IReadOnlyList<Commons.Entities.Templates.TemplateComposition>>();
        var composedChains = new Dictionary<int, IReadOnlyList<Commons.Entities.Templates.Template>>();
        var processedTemplateIds = new HashSet<int>();
        var pendingChains = new Queue<IReadOnlyList<Commons.Entities.Templates.Template>>();

        pendingChains.Enqueue(templateChain);
        while (pendingChains.Count > 0)
        {
            var chain = pendingChains.Dequeue();
            foreach (var template in chain)
            {
                if (!processedTemplateIds.Add(template.Id)) continue;

                var compositions = await session.GetCompositionsAsync(
                    template.Id,
                    id => _templateRepo.GetCompositionsByTemplateIdAsync(id, cancellationToken));
                if (compositions.Count == 0) continue;

                compositionMap[template.Id] = compositions;
                foreach (var comp in compositions)
                {
                    if (composedChains.ContainsKey(comp.ComposedTemplateId)) continue;

                    var composedChain = await session.GetChainAsync(comp.ComposedTemplateId, LoadTemplate);
                    composedChains[comp.ComposedTemplateId] = composedChain;
                    pendingChains.Enqueue(composedChain);
                }
            }
        }

        // Load data connections for the site
        var dataConnections = await session.GetDataConnectionsAsync(
            instance.SiteId,
            siteId => LoadDataConnectionsAsync(siteId, cancellationToken));

        // Flatten
        var flattenResult = _flatteningService.Flatten(
            instance, templateChain, compositionMap, composedChains, dataConnections);

        if (flattenResult.IsFailure)
            return Result<FlatteningPipelineResult>.Failure(flattenResult.Error);

        var config = flattenResult.Value;

        // Load shared scripts for semantic validation. Session-global: identical
        // for every instance in a batch, so hoisted to one query per session
        // (the ArtifactDeploymentService global-hoist shape).
        var resolvedSharedScripts = await session.GetSharedScriptsAsync(async () =>
        {
            var sharedScriptEntities = await _templateRepo.GetAllSharedScriptsAsync(cancellationToken);
            return (IReadOnlyList<ResolvedScript>)sharedScriptEntities.Select(s => new ResolvedScript
            {
                CanonicalName = s.Name,
                Code = s.Code,
                ParameterDefinitions = s.ParameterDefinitions,
                ReturnDefinition = s.ReturnDefinition
            }).ToList();
        });

        // Compute the alarm-capable connection-name set so the semantic validator
        // can gate native-alarm-source bindings. "Alarm-capable" matches the DCL
        // runtime decision (DataConnectionActor: _adapter is IAlarmSubscribableConnection);
        // here we filter connections by alarm-capable protocol, then collect their names.
        //
        // StringComparer.Ordinal is intentional: connection names are stored and
        // matched as authored throughout the pipeline (all other name-keyed
        // dictionaries in FlatteningService and SemanticValidator use the same
        // case-sensitive semantics). OrdinalIgnoreCase would be inconsistent with
        // the rest of the binding-resolution path.
        var alarmCapableConnectionNames = dataConnections.Values
            .Where(c => AlarmCapableProtocols.IsAlarmCapable(c.Protocol))
            .Select(c => c.Name)
            .ToHashSet(StringComparer.Ordinal);

        // The set of data-connection names that actually exist on the
        // target site, used to verify each bound connection resolves to a real site
        // connection. Same StringComparer.Ordinal as the rest of the binding-resolution
        // path (connection names are matched as-authored throughout the pipeline).
        var siteConnectionNames = dataConnections.Values
            .Select(c => c.Name)
            .ToHashSet(StringComparer.Ordinal);

        // Build the JSON-Schema $ref resolution seam from the shared-schema
        // library. The seam ValidationService consumes is synchronous, so the library is
        // pre-loaded once into a name→JSON map here (avoiding sync-over-async) and the
        // seam is a pure in-memory lookup. An unresolved {"$ref":"lib:Name"} in any
        // validated script schema then becomes a deploy-blocking SchemaReference error.
        // Session-global, same as the shared scripts above.
        var schemaLibrary = await session.GetSchemaLibraryAsync(async () =>
        {
            var sharedSchemas = await _sharedSchemaRepo.ListAsync(cancellationToken);
            return (IReadOnlyDictionary<string, string>)sharedSchemas
                .ToDictionary(s => s.Name, s => s.SchemaJson, StringComparer.Ordinal);
        });
        Func<string, string?> resolveSchemaRef = name => schemaLibrary.GetValueOrDefault(name);

        // Validate. This is the deploy-gating path, so connection-binding completeness
        // is enforced as an Error (enforceConnectionBindings: true): a data-sourced
        // attribute with no binding — or one bound to a connection that no longer exists
        // on the site — blocks the deployment. (The template DESIGN-TIME validate path in
        // ManagementActor leaves this non-blocking by NOT enforcing, since bindings are
        // set later at instance/deploy time.)
        // validateScripts == false is the read-only staleness/comparison path: skip
        // only the expensive Roslyn ValidateScriptCompilation (which loads a
        // non-collectible InteractiveAssemblyLoader assembly per script). Structural
        // and semantic validation still run so a genuinely broken flatten is surfaced;
        // the deploy path (validateScripts: true) remains the authoritative compile gate.
        var validation = _validationService.Validate(
            config,
            resolvedSharedScripts,
            alarmCapableConnectionNames,
            enforceConnectionBindings: true,
            siteConnectionNames: siteConnectionNames,
            resolveSchemaRef: resolveSchemaRef,
            validateScriptCompilation: validateScripts);

        // Compute revision hash
        var hash = _revisionHashService.ComputeHash(config);

        return Result<FlatteningPipelineResult>.Success(
            new FlatteningPipelineResult(config, hash, validation));
    }

    private async Task<IReadOnlyDictionary<int, DataConnection>> LoadDataConnectionsAsync(
        int siteId,
        CancellationToken cancellationToken)
    {
        var connections = await _siteRepo.GetDataConnectionsBySiteIdAsync(siteId, cancellationToken);
        return connections.ToDictionary(c => c.Id);
    }
}
