using System.Data;
using System.Data.Common;
using Hc900Crawler.Core.Application.Feedforward;
using Hc900Crawler.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Hc900Crawler.Infrastructure.Control;

public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
{
    private readonly Hc900DbContext _ctx;
    private readonly ILogger<FeedforwardConfigStore> _logger;

    public FeedforwardConfigStore(Hc900DbContext ctx, ILogger<FeedforwardConfigStore> logger)
    { _ctx = ctx; _logger = logger; }

    public async Task<IReadOnlyList<ColumnConfig>> LoadAllAsync(CancellationToken ct = default)
    {
        var conn = _ctx.Database.GetDbConnection();
        if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);

        var cols = new Dictionary<int, (ColumnConfig cfg, List<StreamConfig> streams)>();
        await using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = """
                SELECT id, name, enabled, feed_tag, pressure_tag, level_tags, scan_sec,
                       feed_filter_tau_sec, feed_move_thr_per_min,
                       press_filter_tau_sec, pressure_band, settle_sec,
                       stale_sec, product_key,
                       temp_tags, sensitive_tray_tag, dtdp, p_ref, steam_op_tag,
                       theta_auto_tune, bias_ma_window_sec,
                       recovery_enabled, recovery_auto_arm,
                       imbalance_trigger_frac, imbalance_trigger_sec,
                       recovery_settle_sec, return_ramp_sec, feed_recovery_sp,
                       delta_p_tag, delta_p_flood_limit,
                       advisory_only,
                       temp_high_limit
                FROM ff_column_config
                """;
            await using var rd = await cmd.ExecuteReaderAsync(ct);
            while (await rd.ReadAsync(ct))
            {
                var levelTags = rd.IsDBNull(5)
                    ? Array.Empty<string>()
                    : rd.GetString(5)
                        .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

                var rawTempTags = rd.IsDBNull(14) ? null : rd.GetString(14);
                var tempTags = rawTempTags is null
                    ? Array.Empty<string>()
                    : rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

                var cfg = new ColumnConfig
                {
                    Id                      = rd.GetInt32(0),
                    Name                    = rd.GetString(1),
                    Enabled                 = rd.GetBoolean(2),
                    AdvisoryOnly            = rd.GetBoolean(30),
                    FeedTag                 = rd.GetString(3),
                    PressureTag             = rd.IsDBNull(4) ? null : rd.GetString(4),
                    LevelTags               = levelTags,
                    ScanSec                 = rd.GetDouble(6),
                    FeedFilterTauSec        = rd.GetDouble(7),
                    FeedMoveThresholdPerMin = rd.GetDouble(8),
                    PressFilterTauSec       = rd.GetDouble(9),
                    PressureBand            = rd.GetDouble(10),
                    SettleSec               = rd.GetDouble(11),
                    StaleSec                = rd.GetDouble(12),
                    ProductKey              = rd.GetString(13),
                    Streams                 = Array.Empty<StreamConfig>(),
                    TempTags                = tempTags,
                    SensitiveTrayTag        = rd.IsDBNull(15) ? null : rd.GetString(15),
                    DTdP                    = rd.GetDouble(16),
                    PRef                    = rd.IsDBNull(17) ? double.NaN : rd.GetDouble(17),
                    SteamOpTag              = rd.IsDBNull(18) ? null : rd.GetString(18),
                    ThetaAutoTune           = rd.GetBoolean(19),
                    BiasMaWindowSec         = rd.GetDouble(20),
                    RecoveryEnabled         = rd.GetBoolean(21),
                    RecoveryAutoArm         = rd.GetBoolean(22),
                    ImbalanceTriggerFrac    = rd.GetDouble(23),
                    ImbalanceTriggerSec     = rd.GetDouble(24),
                    RecoverySettleSec       = rd.GetDouble(25),
                    ReturnRampSec           = rd.GetDouble(26),
                    FeedRecoverySp          = rd.GetDouble(27),
                    DeltaPTag               = rd.IsDBNull(28) ? null : rd.GetString(28),
                    DeltaPFloodLimit        = rd.GetDouble(29),
                    TempHighLimit           = rd.GetDouble(31),
                };
                cols[cfg.Id] = (cfg, new List<StreamConfig>());
            }
        }

        await using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = """
                SELECT column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec,
                       tau_sec, sp_min, sp_max, rate_up_per_min, rate_dn_per_min,
                       reflux_from_product, grade, level_tag,
                       is_reflux, recovery_sp, sp_node_id
                FROM ff_stream_config
                ORDER BY id
                """;
            await using var rd = await cmd.ExecuteReaderAsync(ct);
            while (await rd.ReadAsync(ct))
            {
                int colId = rd.GetInt32(0);
                if (!cols.TryGetValue(colId, out var entry)) continue;
                entry.streams.Add(new StreamConfig
                {
                    Key               = rd.GetString(1),
                    FlowTag           = rd.GetString(2),
                    Role              = Enum.TryParse<StreamRole>(rd.GetString(3), true, out var role) ? role : StreamRole.Monitor,
                    LevelTag          = rd.IsDBNull(14) ? null : rd.GetString(14),
                    IsReflux          = rd.GetBoolean(15),
                    RecoverySp        = rd.IsDBNull(16) ? double.NaN : rd.GetDouble(16),
                    SpNodeId          = rd.IsDBNull(17) ? null : rd.GetString(17),
                    TargetCoeff       = rd.GetDouble(4),
                    ThetaUpSec        = rd.GetDouble(5),
                    ThetaDnSec        = rd.GetDouble(6),
                    TauSec            = rd.GetDouble(7),
                    SpMin             = rd.GetDouble(8),
                    SpMax             = rd.GetDouble(9),
                    RateUpPerMin      = rd.GetDouble(10),
                    RateDnPerMin      = rd.GetDouble(11),
                    RefluxFromProduct = rd.GetBoolean(12),
                    Grade             = Enum.TryParse<Confidence>(rd.GetString(13), true, out var g) ? g : Confidence.A
                });
            }
        }

        return cols.Values.Select(e => e.cfg with { Streams = e.streams }).ToList();
    }

    private static DbParameter P(DbCommand cmd, string name, object? val)
    {
        var p = cmd.CreateParameter();
        p.ParameterName = name;
        p.Value = val ?? DBNull.Value;
        cmd.Parameters.Add(p);
        return p;
    }

    public async Task<int> SaveColumnAsync(ColumnConfig cfg, CancellationToken ct = default)
    {
        var conn = _ctx.Database.GetDbConnection();
        if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
        await using var tx = await conn.BeginTransactionAsync(ct);

        int id = cfg.Id;
        var levelTags = string.Join(',', cfg.LevelTags);

        if (id == 0)
        {
            await using var cmd = conn.CreateCommand();
            cmd.Transaction = tx;
            cmd.CommandText = """
                INSERT INTO ff_column_config
                  (name, enabled, feed_tag, pressure_tag, level_tags, scan_sec,
                   feed_filter_tau_sec, feed_move_thr_per_min, press_filter_tau_sec,
                   pressure_band, settle_sec, stale_sec, product_key, advisory_only,
                   temp_tags, sensitive_tray_tag, dtdp, p_ref, steam_op_tag,
                   theta_auto_tune, bias_ma_window_sec,
                   recovery_enabled, recovery_auto_arm,
                   imbalance_trigger_frac, imbalance_trigger_sec,
                   recovery_settle_sec, return_ramp_sec, feed_recovery_sp,
                   delta_p_tag, delta_p_flood_limit, temp_high_limit)
                VALUES (@name,@en,@feed,@pres,@lvl,@scan,@fft,@fmt,@pft,@pb,@settle,@stale,@pk,@advisory,
                        @tempTags,@sensTray,@dtdp,@pRef,@steamOp,
                        @thetaAuto,@biasMaWin,
                        @recEn,@recAutoArm,
                        @imbFrac,@imbSec,
                        @recSettle,@retRamp,@feedRecSp,
                        @deltaPTag,@deltaPFlood,@tempHigh)
                RETURNING id
                """;
            P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled); P(cmd,"@feed",cfg.FeedTag);
            P(cmd,"@pres",(object?)cfg.PressureTag); P(cmd,"@lvl",levelTags);
            P(cmd,"@advisory",cfg.AdvisoryOnly);
            P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec); P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin);
            P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand); P(cmd,"@settle",cfg.SettleSec);
            P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
            P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
            P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
            P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
            P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
            P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
            P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
            P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
            P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
            P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
            P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
            P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
            P(cmd,"@tempHigh",cfg.TempHighLimit);
            id = Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
        }
        else
        {
            await using var cmd = conn.CreateCommand();
            cmd.Transaction = tx;
            cmd.CommandText = """
                UPDATE ff_column_config SET
                  name=@name, enabled=@en, feed_tag=@feed, pressure_tag=@pres, level_tags=@lvl,
                  scan_sec=@scan, feed_filter_tau_sec=@fft, feed_move_thr_per_min=@fmt,
                  press_filter_tau_sec=@pft, pressure_band=@pb, settle_sec=@settle,
                  stale_sec=@stale, product_key=@pk, advisory_only=@advisory,
                  temp_tags=@tempTags, sensitive_tray_tag=@sensTray, dtdp=@dtdp, p_ref=@pRef,
                  steam_op_tag=@steamOp, theta_auto_tune=@thetaAuto, bias_ma_window_sec=@biasMaWin,
                  recovery_enabled=@recEn, recovery_auto_arm=@recAutoArm,
                  imbalance_trigger_frac=@imbFrac, imbalance_trigger_sec=@imbSec,
                  recovery_settle_sec=@recSettle, return_ramp_sec=@retRamp, feed_recovery_sp=@feedRecSp,
                  delta_p_tag=@deltaPTag, delta_p_flood_limit=@deltaPFlood, temp_high_limit=@tempHigh
                WHERE id=@id
                """;
            P(cmd,"@id",id); P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled);
            P(cmd,"@feed",cfg.FeedTag); P(cmd,"@pres",(object?)cfg.PressureTag);
            P(cmd,"@advisory",cfg.AdvisoryOnly);
            P(cmd,"@lvl",levelTags); P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec);
            P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin); P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand);
            P(cmd,"@settle",cfg.SettleSec); P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
            P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
            P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
            P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
            P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
            P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
            P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
            P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
            P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
            P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
            P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
            P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
            P(cmd,"@tempHigh",cfg.TempHighLimit);
            await cmd.ExecuteNonQueryAsync(ct);
        }

        // 스트림 원자적 교체
        await using (var del = conn.CreateCommand())
        {
            del.Transaction = tx; del.CommandText = "DELETE FROM ff_stream_config WHERE column_id=@id";
            P(del,"@id",id); await del.ExecuteNonQueryAsync(ct);
        }
        foreach (var s in cfg.Streams)
        {
            await using var ins = conn.CreateCommand();
            ins.Transaction = tx;
            ins.CommandText = """
                INSERT INTO ff_stream_config
                  (column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec, tau_sec,
                   sp_min, sp_max, rate_up_per_min, rate_dn_per_min, reflux_from_product, grade, level_tag,
                   is_reflux, recovery_sp, sp_node_id)
                VALUES (@cid,@key,@flow,@role,@k,@tup,@tdn,@tau,@smin,@smax,@rup,@rdn,@rfp,@grade,@lvlTag,
                        @isReflux,@recSp,@spNode)
                """;
            P(ins,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag);
            P(ins,"@role",s.Role.ToString()); P(ins,"@lvlTag",(object?)s.LevelTag ?? DBNull.Value); P(ins,"@k",s.TargetCoeff); P(ins,"@tup",s.ThetaUpSec);
            P(ins,"@tdn",s.ThetaDnSec); P(ins,"@tau",s.TauSec); P(ins,"@smin",s.SpMin); P(ins,"@smax",s.SpMax);
            P(ins,"@rup",s.RateUpPerMin); P(ins,"@rdn",s.RateDnPerMin); P(ins,"@rfp",s.RefluxFromProduct);
            P(ins,"@grade",s.Grade.ToString());
            P(ins,"@isReflux",s.IsReflux); P(ins,"@recSp",double.IsNaN(s.RecoverySp) ? DBNull.Value : (object)s.RecoverySp);
            P(ins,"@spNode",(object?)s.SpNodeId ?? DBNull.Value);
            await ins.ExecuteNonQueryAsync(ct);
        }

        await tx.CommitAsync(ct);
        return id;
    }

    public async Task DeleteColumnAsync(int columnId, CancellationToken ct = default)
    {
        var conn = _ctx.Database.GetDbConnection();
        if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
        await using var cmd = conn.CreateCommand();
        cmd.CommandText = "DELETE FROM ff_column_config WHERE id=@id";
        P(cmd,"@id",columnId);
        await cmd.ExecuteNonQueryAsync(ct);
    }
}
