using Hc900Crawler.Core.Application.Feedforward;
using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Core.Domain.Entities;
using Hc900Crawler.Infrastructure.Hc900;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Globalization;

namespace Hc900Crawler.Infrastructure.Control;

public sealed class FeedforwardSupervisor : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly FeedforwardEngine     _engine;
    private readonly IFeedforwardAdvisoryStore _store;
    private readonly IFeedforwardWriteGuard _writeGuard;
    private readonly ILogger<FeedforwardSupervisor> _logger;
    private readonly Microsoft.Extensions.Configuration.IConfiguration _appConfig;
    private readonly ISimOverrideStore _sim;   // WP0 확장: 엔진 스냅샷 입력 치환(DEMO)
    private readonly Dictionary<int, ColumnState> _states = new();
    // Phase II: 마지막 쓰기 시각(스트림별 rate-limit) 및 결과
    private readonly ConcurrentDictionary<(int colId, string streamKey), DateTime> _lastWriteTimes = new();
    private readonly ConcurrentDictionary<(int colId, string streamKey), (double? sp, string? error, DateTime? at)> _lastWriteResults = new();

    public FeedforwardSupervisor(
        IServiceScopeFactory scopeFactory, FeedforwardEngine engine,
        IFeedforwardAdvisoryStore store, IFeedforwardWriteGuard writeGuard,
        ILogger<FeedforwardSupervisor> logger,
        Microsoft.Extensions.Configuration.IConfiguration appConfig,
        ISimOverrideStore sim, ICompositionStore composition)
    { _scopeFactory = scopeFactory; _engine = engine; _store = store; _writeGuard = writeGuard; _logger = logger; _appConfig = appConfig; _sim = sim; _composition = composition; }

    private readonly ICompositionStore _composition;

    // WP5 3단계: LevelDriven 스트림의 분율을 수동입력(랩)값으로 치환(있으면). 없으면 config K.
    private ColumnConfig ApplyManualFractions(ColumnConfig cfg)
    {
        if (cfg.Streams.All(s => s.Role != StreamRole.LevelDriven)) return cfg;
        bool any = false;
        var streams = cfg.Streams.Select(s =>
        {
            if (s.Role == StreamRole.LevelDriven && _composition.TryGet(cfg.Id, s.Key, out var frac))
            { any = true; return s with { TargetCoeff = frac }; }
            return s;
        }).ToList();
        return any ? cfg with { Streams = streams } : cfg;
    }

    // Phase II: 쓰기 결과 조회 (Controller에서 사용)
    public (double? sp, string? error, DateTime? at) GetLastWrite(int colId, string streamKey)
        => _lastWriteResults.TryGetValue((colId, streamKey), out var r) ? r : (null, null, null);

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await Task.Yield();
        while (!ct.IsCancellationRequested)
        {
            double minScan = 2.0;
            try
            {
                using var scope = _scopeFactory.CreateScope();
                var cfgStore = scope.ServiceProvider.GetRequiredService<IFeedforwardConfigStore>();
                var db       = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
                var writeClient = scope.ServiceProvider.GetService<Hc900WriteService>();
                var auditService = scope.ServiceProvider.GetService<IFeedforwardAuditService>();

                var columns = await cfgStore.LoadAllAsync(ct);
                var enabled = columns.Where(c => c.Enabled).ToList();
                if (enabled.Count > 0) minScan = enabled.Min(c => c.ScanSec);

                foreach (var cfg0 in enabled)
                {
                    var cfg = ApplyManualFractions(cfg0);   // WP5 3단계: 랩 수동입력 분율 치환
                    try
                    {
                        var snap = await BuildSnapshotAsync(db, cfg);
                        var st   = GetState(cfg.Id);
                        var res  = _engine.Tick(cfg, snap, st, DateTime.UtcNow);
                        // Phase II: auto-write
                        // 안전가드: Sim Override 활성 시 입력이 가짜이므로 실제 쓰기 금지(advisory-only로 강등)
                        if (!cfg.AdvisoryOnly && writeClient is not null && auditService is not null && !_sim.Enabled)
                        {
                            await AutoWriteAsync(cfg, res, st, writeClient, auditService, ct);
                            res = res with { AutoWriteActive = true };
                        }
                        else if (!cfg.AdvisoryOnly && _sim.Enabled)
                            _logger.LogWarning("[FF] Sim Override 활성 — col {Id} auto-write 억제(가짜 입력)", cfg.Id);
                        _store.Set(res);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogWarning(ex, "FF tick 실패: column {Id}", cfg.Id);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "FF supervisor 루프 오류");
            }
            await Task.Delay(TimeSpan.FromSeconds(Math.Clamp(minScan, 1.0, 10.0)), ct);
        }
    }

    // ── Phase II: auto-write ─────────────────────────────────────────────
    private async Task AutoWriteAsync(ColumnConfig cfg, AdvisoryResult column, ColumnState st,
        Hc900WriteService writeClient, IFeedforwardAuditService audit, CancellationToken ct)
    {
        if (column.Transient)
            return;

        foreach (var s in cfg.Streams)
        {
            if (s.Role != StreamRole.Commanded) continue;
            if (string.IsNullOrWhiteSpace(s.SpNodeId)) continue;  // 쓰기 대상 미지정

            var adv = column.Streams.FirstOrDefault(a => a.Key == s.Key);
            if (adv is null) continue;

            // 1) WriteGuard 검증
            var check = _writeGuard.Check(cfg, adv, s, column);
            if (!check.Allowed)
            {
                // 차단 로그
                _lastWriteResults[(cfg.Id, s.Key)] = (adv.RecommendedSp, check.Reason, DateTime.UtcNow);
                await audit.LogAsync(new FfActionLogEntry(cfg.Id, "sp_write",
                    StreamKey: s.Key, SpValue: adv.RecommendedSp,
                    NodeId: s.SpNodeId, Result: "blocked",
                    WriteguardReason: check.Reason), ct);
                continue;
            }

            // 2) Rate-limit: 최소 ScanSec*2 간격
            var lastWrite = _lastWriteTimes.GetValueOrDefault((cfg.Id, s.Key), DateTime.MinValue);
            var minInterval = TimeSpan.FromSeconds(Math.Max(cfg.ScanSec * 2, 2.0));
            if (DateTime.UtcNow - lastWrite < minInterval) continue;

            // 3) HC900 gRPC 쓰기 (SpNodeId를 HC900 태그명으로 사용)
            double spVal = adv.RecommendedSp!.Value;
            var (success, error) = await writeClient.WriteTagAsync("HC1", s.SpNodeId, spVal);

            // 4) 결과 저장
            _lastWriteTimes[(cfg.Id, s.Key)] = DateTime.UtcNow;
            if (success)
            {
                _lastWriteResults[(cfg.Id, s.Key)] = (spVal, null, DateTime.UtcNow);
                _logger.LogInformation("[FF] SP 쓰기 성공 col={Col} stream={Key} node={Node} val={Val:F2}",
                    cfg.Id, s.Key, s.SpNodeId, spVal);
            }
            else
            {
                _lastWriteResults[(cfg.Id, s.Key)] = (spVal, error, DateTime.UtcNow);
                _logger.LogWarning("[FF] SP 쓰기 실패 col={Col} stream={Key} node={Node} err={Err}",
                    cfg.Id, s.Key, s.SpNodeId, error);
            }

            await audit.LogAsync(new FfActionLogEntry(cfg.Id, "sp_write",
                StreamKey: s.Key, SpValue: spVal, NodeId: s.SpNodeId,
                Result: success ? "success" : $"error: {error}"), ct);
        }
    }


    private ColumnState GetState(int id)
    {
        if (!_states.TryGetValue(id, out var s)) { s = new ColumnState(); _states[id] = s; }
        return s;
    }

    // WO-6: 운전원 ARM/취소 (모드 판정용 플래그만 — 쓰기 아님). 다음 Tick에서 소비.
    public bool Arm(int columnId)    { lock (_states) { GetState(columnId).OperatorArmed = true; }  return true; }
    public bool Cancel(int columnId) { lock (_states) { GetState(columnId).OperatorCancel = true; } return true; }

    private async Task<PvSnapshot> BuildSnapshotAsync(IExperionDbService db, ColumnConfig cfg)
    {
        string PvTag(string baseTag)
        {
            if (string.IsNullOrWhiteSpace(baseTag)) return baseTag;   // 빈 태그는 그대로(호출부가 처리)
            var result = baseTag.EndsWith(".pv", StringComparison.OrdinalIgnoreCase) ? baseTag : baseTag + ".pv";
            return result.ToUpperInvariant();   // register map 기준 대문자 통일
        }
        var feedTag = PvTag(cfg.FeedTag);
        if (string.IsNullOrWhiteSpace(feedTag))
        {
            _logger.LogWarning("[FF] FeedTag 비어 있음 — column {Id} 처리 불가", cfg.Id);
            return new PvSnapshot(
                new TagSample("", double.NaN, Good: false, DateTime.MinValue),
                null, Array.Empty<TagSample>(), new Dictionary<string, TagSample>());
        }
        var tags = new List<string> { feedTag };
        if (cfg.PressureTag is not null) tags.Add(PvTag(cfg.PressureTag));
        tags.AddRange(cfg.LevelTags.Select(PvTag));
        tags.AddRange(cfg.Streams.Where(s => s.LevelTag is not null).Select(s => PvTag(s.LevelTag!)));
        tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
        tags.AddRange(cfg.TempTags.Select(PvTag));   // WO-2 온도 프로파일
        if (cfg.SteamOpTag is not null) tags.Add(cfg.SteamOpTag.ToUpperInvariant());  // WO-3 스팀 OP(.op)
        if (cfg.DeltaPTag is not null)  tags.Add(PvTag(cfg.DeltaPTag));               // WO-6 차압(.pv)

        // WP6: DeltaPTag 미지정 시 PressureTag로부터 탑저압 파생
        string? derivedBottomTag = null;
        if (cfg.DeltaPTag is null && cfg.PressureTag is not null)
        {
            // pica-{n} → pi-{n}b (C-6111 명명 규약)
            var p = cfg.PressureTag;
            if (p.StartsWith("pica-", StringComparison.OrdinalIgnoreCase))
            {
                var num = p["pica-".Length..];
                derivedBottomTag = "pi-" + num + "b";
                tags.Add(PvTag(derivedBottomTag));
            }
        }

        var rows = (await db.GetRealtimeRecordsByTagNamesAsync(tags))
                   .ToDictionary(r => r.TagName, r => r);

        TagSample Sample(string baseTag)
        {
            var tag = PvTag(baseTag);
            if (_sim.Enabled && _sim.TryGet(tag, out var sov))   // WP0 확장: override 우선(신선 처리)
                return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
            if (rows.TryGetValue(tag, out var r)
                && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
            {
                bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec;
                return new TagSample(tag, v, Good: fresh, r.Timestamp);
            }
            return new TagSample(tag, double.NaN, Good: false, DateTime.MinValue);
        }

        // WO-3: .op 등 비-.pv 태그를 접미사 강제 없이 그대로 읽음
        TagSample SampleExact(string rawTag)
        {
            var tag = rawTag.ToUpperInvariant();   // register map 기준 대문자 통일
            if (_sim.Enabled && _sim.TryGet(tag, out var sov))   // WP0 확장: override 우선
                return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
            if (rows.TryGetValue(tag, out var r)
                && double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
            {
                bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec;
                return new TagSample(tag, v, Good: fresh, r.Timestamp);
            }
            return new TagSample(tag, double.NaN, Good: false, DateTime.MinValue);
        }

        var feed = Sample(cfg.FeedTag);
        var press = cfg.PressureTag is null ? null : Sample(cfg.PressureTag);
        var levels = cfg.LevelTags.Select(Sample).ToList();
        var streams = cfg.Streams.ToDictionary(s => s.Key, s => Sample(s.FlowTag));
        var temps = cfg.TempTags.Count > 0 ? cfg.TempTags.Select(Sample).ToList() : null;
        var steam = cfg.SteamOpTag is not null ? SampleExact(cfg.SteamOpTag) : null;

        // WP6: ΔP 합성 — delta_p_tag 직접값 또는 {bottom - top} 파생
        TagSample? deltaP;
        TagSample? bottomPress = null;
        if (cfg.DeltaPTag is not null)
        {
            deltaP = Sample(cfg.DeltaPTag);
        }
        else if (press is { Good: true } && derivedBottomTag is not null)
        {
            bottomPress = Sample(derivedBottomTag);
            if (bottomPress.Good && Num.IsFinite(bottomPress.Value) && Num.IsFinite(press.Value))
            {
                double dpVal = bottomPress.Value - press.Value;
                deltaP = new TagSample("_synthetic_dp", dpVal, Good: true, DateTime.UtcNow);
            }
            else deltaP = null;
        }
        else deltaP = null;

        return new PvSnapshot(feed, press, levels, streams)
            { Temps = temps, SteamOp = steam, DeltaP = deltaP, BottomPressure = bottomPress };
    }
}
