using Hc900Crawler.Core.Application.Feedforward;
using Hc900Crawler.Core.Application.Interfaces;
using Hc900Crawler.Infrastructure.Hc900;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Hc900Crawler.Infrastructure.Control;

/// <summary>
/// 작업 B: FEED 램프 실행기. 운전원이 시작한 활성 Job에 대해 주기마다
/// FeedRampCalculator 결과(RampRate·Ceiling)에 따라 FEED SP를 단계적으로 써 나간다.
/// 안전: 피드 불량 HOLD / 범위 클램프 / 단조진행(다운 점프 금지) / DryRun / SimOverride 억제.
/// </summary>
public sealed class FeedRampExecutorService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IFeedRampJobStore    _jobs;
    private readonly Hc900WriteService    _writeClient;
    private readonly ISimOverrideStore    _sim;
    private readonly IConfiguration       _appConfig;
    private readonly ILogger<FeedRampExecutorService> _logger;

    private const double Eps = 1e-6;

    public FeedRampExecutorService(
        IServiceScopeFactory scopeFactory, IFeedRampJobStore jobs,
        Hc900WriteService writeClient, ISimOverrideStore sim,
        IConfiguration appConfig, ILogger<FeedRampExecutorService> logger)
    {
        _scopeFactory = scopeFactory; _jobs = jobs; _writeClient = writeClient;
        _sim = sim; _appConfig = appConfig; _logger = logger;
    }

    private int StepIntervalSec => Math.Clamp(_appConfig.GetValue<int?>("Feedforward:FeedRampStepSec") ?? 10, 2, 120);
    private bool GlobalDryRun => _appConfig.GetValue<bool?>("Feedforward:FeedRampDryRun") ?? true;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await Task.Yield();
        while (!ct.IsCancellationRequested)
        {
            try
            {
                var active = _jobs.GetAll()
                    .Where(j => j.State == FeedRampState.Ramping || j.State == FeedRampState.Hold)
                    .ToList();
                if (active.Count > 0)
                {
                    using var scope = _scopeFactory.CreateScope();
                    var cfgStore = scope.ServiceProvider.GetRequiredService<IFeedforwardConfigStore>();
                    var ramp     = scope.ServiceProvider.GetRequiredService<FeedRampAdvisorService>();
                    var audit    = scope.ServiceProvider.GetService<IFeedforwardAuditService>();
                    var db       = scope.ServiceProvider.GetRequiredService<IExperionDbService>();
                    var cfgs     = await cfgStore.LoadAllAsync(ct);

                    foreach (var job in active)
                    {
                        var cfg = cfgs.FirstOrDefault(c => c.Id == job.ColumnId);
                        try { await StepJobAsync(job, cfg, ramp, audit, db, ct); }
                        catch (Exception ex) { _logger.LogWarning(ex, "[FeedRamp] step 실패 col={Col}", job.ColumnId); }
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "[FeedRamp] 실행기 루프 오류");
            }
            await Task.Delay(TimeSpan.FromSeconds(StepIntervalSec), ct);
        }
    }

    private async Task StepJobAsync(FeedRampJob job, ColumnConfig? cfg,
        FeedRampAdvisorService ramp, IFeedforwardAuditService? audit, IExperionDbService db, CancellationToken ct)
    {
        if (cfg is null)
        {
            _jobs.Update(job with { State = FeedRampState.Canceled, Hold = "config 없음" });
            return;
        }
        // FEED SP 대상 = Feed 태그에서 WSP(.SP) 자동 파생. FeedSpNodeId는 선택적 override.
        var feedSpTag = FfSpTag.Resolve(cfg.FeedTag, cfg.FeedSpNodeId);
        if (string.IsNullOrWhiteSpace(feedSpTag))
        {
            _jobs.Update(job with { State = FeedRampState.Canceled, Hold = "Feed 태그 없음 — SP 대상 불가" });
            return;
        }

        // SimOverride 활성 시 입력이 가짜이므로 실쓰기 억제(강제 DryRun)
        bool dryRun = job.DryRun || GlobalDryRun || _sim.Enabled;

        double floodLimit = (cfg.DeltaPFloodLimit < double.MaxValue && cfg.DeltaPFloodLimit > 0)
            ? cfg.DeltaPFloodLimit : double.NaN;
        var adv = await ramp.ComputeAsync(cfg.Id, job.TargetFeed,
            deltaIAllow: 50, sensibleGain: double.NaN, feedTempRef: double.NaN,
            floodLimit: floodLimit, n: 1.8, ct);

        if (adv is null)
        {
            _jobs.Update(job with { State = FeedRampState.Hold, Hold = "advisory 계산 불가" });
            return;
        }

        // 피드 불량 → HOLD (다음 단계 안 씀, elapsed 리셋해 재개 시 점프 방지)
        if (adv.Hold)
        {
            _jobs.Update(job with
            {
                State = FeedRampState.Hold,
                Hold = adv.Warnings.FirstOrDefault() ?? "피드 불량",
                CurrentFeed = double.IsFinite(adv.CurrentFeed) ? adv.CurrentFeed : null,
                LastStepAt = DateTime.UtcNow,
                Warnings = adv.Warnings
            });
            return;
        }

        double currentFeed = adv.CurrentFeed;
        double rampRate    = adv.RampRate.Value;
        double ceiling     = adv.Ceiling.Value;
        double targetCap   = Math.Min(job.TargetFeed, double.IsFinite(ceiling) ? ceiling : job.TargetFeed);

        // 앵커: 첫 단계 — LastWrittenSp를 현재 FEED WSP(설정값)로 시드(범프리스 시작).
        // ※ 측정 PV(currentFeed)는 데드타임·필터 지연값이므로 앵커로 쓰지 않음. SP는 WSP에서 출발해
        //   램프율로 시간 적분한 궤적을 추종한다(PV 비의존). WSP 조회 불가 시에만 PV로 폴백.
        if (double.IsNaN(job.LastWrittenSp))
        {
            double? curWsp = await TryReadValueAsync(db, feedSpTag);
            double anchor = curWsp ?? (double.IsFinite(currentFeed) ? currentFeed : cfg.FeedSpMin);
            anchor = Math.Clamp(anchor, cfg.FeedSpMin, ClampMax(cfg.FeedSpMax));
            _jobs.Update(job with
            {
                LastWrittenSp = anchor, LastStepAt = DateTime.UtcNow,
                CurrentFeed = double.IsFinite(currentFeed) ? currentFeed : null,
                Ceiling = double.IsFinite(ceiling) ? ceiling : null,
                RampRate = double.IsFinite(rampRate) ? rampRate : null,
                State = FeedRampState.Ramping, Hold = null, Warnings = adv.Warnings
            });
            return;
        }

        // 도달 판정
        if (job.LastWrittenSp >= targetCap - Eps)
        {
            _jobs.Update(job with
            {
                State = FeedRampState.Reached, Hold = null,
                CurrentFeed = double.IsFinite(currentFeed) ? currentFeed : null,
                Ceiling = double.IsFinite(ceiling) ? ceiling : null, Warnings = adv.Warnings
            });
            return;
        }

        // 램프율 필요 — 없으면(무제한/미산정) 안전상 HOLD
        if (!double.IsFinite(rampRate) || rampRate <= 0)
        {
            _jobs.Update(job with
            {
                State = FeedRampState.Hold, Hold = "램프율 미산정/무제한 — 단계 보류",
                Warnings = adv.Warnings
            });
            return;
        }

        var now = DateTime.UtcNow;
        var lastStep = job.LastStepAt ?? job.StartedAt;
        double elapsedMin = (now - lastStep).TotalMinutes;

        // 레이트리밋(§3.4): 최소 ScanSec*2 간격
        double minIntervalSec = Math.Max(cfg.ScanSec * 2, 2.0);
        if ((now - lastStep).TotalSeconds < minIntervalSec) return;

        // 작업플랜-민감단온도: 하강 램프 지원 (adv.RampRate 부호에 따라 방향 결정)
        bool goingDown = targetCap < job.LastWrittenSp - 1e-6;
        double step = rampRate * elapsedMin;
        double nextSp = goingDown
            ? Math.Max(job.LastWrittenSp - step, targetCap)   // 하강: rate는 양수, step만큼 감소
            : Math.Min(job.LastWrittenSp + step, targetCap);  // 상승: 기존
        if (!goingDown)
            nextSp = Math.Max(nextSp, job.LastWrittenSp);     // 상승 단조(다운 점프 금지)
        nextSp = Math.Clamp(nextSp, cfg.FeedSpMin, ClampMax(cfg.FeedSpMax));

        if (double.IsNaN(nextSp) || double.IsInfinity(nextSp))
        {
            _jobs.Update(job with { State = FeedRampState.Hold, Hold = "다음 SP 계산값 NaN/Inf", Warnings = adv.Warnings });
            return;
        }

        // 진전 없음(ceiling 하락 등) → 쓰지 않고 대기
        if (nextSp <= job.LastWrittenSp + Eps)
        {
            _jobs.Update(job with
            {
                CurrentFeed = double.IsFinite(currentFeed) ? currentFeed : null,
                Ceiling = double.IsFinite(ceiling) ? ceiling : null, RampRate = rampRate,
                LastStepAt = now, Warnings = adv.Warnings,
                Hold = double.IsFinite(ceiling) && ceiling < job.LastWrittenSp ? "ceiling 하락 — 현재 SP 유지" : job.Hold
            });
            return;
        }

        bool success; string? error = null;
        if (dryRun)
        {
            success = true;
            _logger.LogInformation("[FeedRamp][DRY] col={Col} tag={Tag} SP {From:F1}→{To:F1} (target {Tgt:F1}, rate {Rate:F1})",
                cfg.Id, feedSpTag, job.LastWrittenSp, nextSp, targetCap, rampRate);
        }
        else
        {
            var ctrlId = await db.GetControllerIdForTagAsync(feedSpTag);   // 태그→컨트롤러 DB 해석
            if (string.IsNullOrWhiteSpace(ctrlId))
            {
                _jobs.Update(job with { State = FeedRampState.Hold, Hold = $"태그 {feedSpTag} 컨트롤러 미해석", RampRate = rampRate, Warnings = adv.Warnings });
                return;
            }
            (success, error) = await _writeClient.WriteTagAsync(ctrlId, feedSpTag, nextSp);
        }

        var warnings = adv.Warnings;
        if (success)
        {
            _jobs.Update(job with
            {
                LastWrittenSp = nextSp, LastStepAt = now, State = FeedRampState.Ramping, Hold = null,
                CurrentFeed = double.IsFinite(currentFeed) ? currentFeed : null,
                Ceiling = double.IsFinite(ceiling) ? ceiling : null, RampRate = rampRate, Warnings = warnings
            });
        }
        else
        {
            _jobs.Update(job with
            {
                LastStepAt = now, Hold = $"쓰기 실패: {error}", RampRate = rampRate,
                Warnings = warnings
            });
        }

        if (audit is not null)
            await audit.LogAsync(new FfActionLogEntry(cfg.Id, "feed_ramp_write",
                SpValue: nextSp, NodeId: feedSpTag,
                Result: dryRun ? "dry-run" : (success ? "success" : $"error: {error}"),
                OperatorName: job.Operator), ct);
    }

    private static double ClampMax(double max) => (double.IsInfinity(max) || max >= double.MaxValue) ? 1e12 : max;

    // realtime_table에서 태그(예: 현재 FEED WSP) 값 1개 best-effort 조회
    private static async Task<double?> TryReadValueAsync(IExperionDbService db, string tag)
    {
        try
        {
            var rows = await db.GetRealtimeRecordsByTagNamesAsync(new[] { tag });
            var r = rows.FirstOrDefault();
            if (r?.LiveValue is not null &&
                double.TryParse(r.LiveValue, System.Globalization.NumberStyles.Float,
                                System.Globalization.CultureInfo.InvariantCulture, out var v))
                return v;
        }
        catch { /* best-effort */ }
        return null;
    }
}
