using System;
using System.Collections.Generic;

namespace QuantEngine.Core.Domain
{
    public class TimingDecisionResult
    {
        public double EntryScore { get; set; }
        public double ExitScore { get; set; }
        public string Action { get; set; } = "HOLD_NO_TIMING_EDGE";
        public string Reason { get; set; } = string.Empty;
    }

    public class SellDecisionResult
    {
        public string Action { get; set; } = "HOLD";
        public double RatioPct { get; set; }
        public double? LimitPrice { get; set; }
        public string PriceSource { get; set; } = string.Empty;
        public string PriceBasis { get; set; } = string.Empty;
        public string ExecutionWindow { get; set; } = string.Empty;
        public string OrderType { get; set; } = string.Empty;
        public string Reason { get; set; } = string.Empty;
        public string Validation { get; set; } = "NO_SELL_ACTION";
        public string CashPreserveStyle { get; set; } = string.Empty;
        public double CashPreserveRatio { get; set; }
        public List<string> CashPreserveReason { get; set; } = new List<string>();
    }

    public class FinalDecisionResult
    {
        public string FinalAction { get; set; } = "HOLD";
        public int ActionPriority { get; set; } = 99;
        public double PriorityScore { get; set; }
        public string DecisionSource { get; set; } = "RULE_ENGINE";
    }

    public class CashShortfallResult
    {
        public double CashCurrentPctD2 { get; set; }
        public double CashTargetPct { get; set; }
        public double CashShortfallMinKrw { get; set; }
        public double CashShortfallTargetKrw { get; set; }
    }

    public class CashRecoveryPlanItem
    {
        public string Ticker { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
        public int Qty { get; set; }
        public double? LimitPrice { get; set; }
        public string PreserveStyle { get; set; } = string.Empty;
        public double PreserveRatio { get; set; }
        public double ExpectedKrw { get; set; }
    }

    public class CashRecoveryPlanResult
    {
        public List<CashRecoveryPlanItem> SellSequence { get; set; } = new List<CashRecoveryPlanItem>();
        public double ExpectedTotalKrw { get; set; }
        public double CashShortfallMinKrw { get; set; }
        public bool ShortfallMet { get; set; }
        public int ItemsNeeded { get; set; }
    }

    public static class FormulaEngine
    {
        private static bool IsValidNumber(double? value)
            => value.HasValue && !double.IsNaN(value.Value) && !double.IsInfinity(value.Value);

        public static TimingDecisionResult ComputeTimingDecision(Dictionary<string, object> ctx)
        {
            var reasons = new List<string>();
            double entryScore = 0;
            double exitScore = 0;

            string entryGate = GetString(ctx, "entryModeGate");
            string entryMode = GetString(ctx, "entryMode");
            string leaderGate = GetString(ctx, "leaderGate");
            string acGate = GetString(ctx, "acGate");
            string exitSignal = GetString(ctx, "exitSignalDetail");
            double? flowCredit = GetNullableDouble(ctx, "flowCredit");
            double? leaderTotal = GetNullableDouble(ctx, "leaderTotal");
            double? rwPartial = GetNullableDouble(ctx, "rwPartial");
            double? rsi14 = GetNullableDouble(ctx, "rsi14");
            double? disparity = GetNullableDouble(ctx, "disparity");
            double? ma20Slope = GetNullableDouble(ctx, "ma20Slope");
            double? spreadPct = GetNullableDouble(ctx, "spreadPct");
            double? avgTradeValue5D = GetNullableDouble(ctx, "avgTradeValue5D");
            double? profitPct = GetNullableDouble(ctx, "profitPct");
            double? daysToTimeStop = GetNullableDouble(ctx, "daysToTimeStop");

            if (entryGate == "PASS")
            {
                entryScore += 25;
                reasons.Add($"entry_{entryMode}");
            }
            else if (entryGate == "BLOCK")
            {
                entryScore -= 25;
                reasons.Add("entry_block");
            }

            if (IsValidNumber(leaderTotal))
            {
                if (leaderTotal!.Value >= 4)
                {
                    entryScore += 20;
                    reasons.Add("leader_scan>=4");
                }
                else if (leaderTotal.Value >= 3)
                {
                    entryScore += 10;
                    reasons.Add("leader_watch");
                }
            }
            if (leaderGate == "PASS" || leaderGate == "EXPLORE_CANDIDATE")
            {
                entryScore += 10;
            }

            if (IsValidNumber(flowCredit))
            {
                if (flowCredit!.Value >= 0.7)
                {
                    entryScore += 20;
                    reasons.Add("flow_strong");
                }
                else if (flowCredit.Value >= 0.4)
                {
                    entryScore += 10;
                    reasons.Add("flow_partial");
                }
            }

            if (acGate == "CLEAR")
            {
                entryScore += 15;
                reasons.Add("anti_climax_clear");
            }
            else if (acGate == "CAUTION")
            {
                entryScore += 5;
                reasons.Add("anti_climax_caution");
            }
            else if (acGate == "BLOCK")
            {
                entryScore -= 35;
                exitScore += 15;
                reasons.Add("anti_climax_block");
            }

            if (IsValidNumber(ma20Slope))
            {
                if (ma20Slope!.Value > 0)
                {
                    entryScore += 8;
                }
                else
                {
                    entryScore -= 8;
                    exitScore += 8;
                    reasons.Add("ma20_down");
                }
            }

            if (IsValidNumber(disparity))
            {
                if (disparity!.Value >= -5 && disparity.Value <= 4)
                {
                    entryScore += 10;
                }
                else if (disparity.Value > 4 && disparity.Value <= 8)
                {
                    entryScore += 5;
                }
                else if (disparity.Value > 12)
                {
                    entryScore -= 25;
                    exitScore += 20;
                    reasons.Add("overextended");
                }
                else if (disparity.Value < -10)
                {
                    entryScore -= 10;
                    exitScore += 10;
                    reasons.Add("trend_damage");
                }
            }

            if (IsValidNumber(rsi14))
            {
                if (rsi14!.Value >= 40 && rsi14.Value <= 65)
                {
                    entryScore += 10;
                }
                else if (rsi14.Value > 65 && rsi14.Value <= 72)
                {
                    entryScore += 4;
                }
                else if (rsi14.Value > 75)
                {
                    entryScore -= 25;
                    exitScore += 20;
                    reasons.Add("rsi_overbought");
                }
                else if (rsi14.Value < 35)
                {
                    entryScore -= 5;
                    exitScore += 8;
                    reasons.Add("weak_rsi");
                }
            }

            if (IsValidNumber(avgTradeValue5D) && avgTradeValue5D!.Value >= 50 && (!IsValidNumber(spreadPct) || spreadPct!.Value <= 0.8))
            {
                entryScore += 10;
            }
            else
            {
                entryScore -= 15;
                reasons.Add("liquidity_or_spread_fail");
            }

            if (IsValidNumber(rwPartial))
            {
                exitScore += Math.Min(100.0, Math.Max(0.0, (int)rwPartial!.Value * 25.0));
            }

            if (!string.IsNullOrEmpty(exitSignal))
            {
                var parts = exitSignal.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
                exitScore += parts.Length * 10;
            }

            if (IsValidNumber(daysToTimeStop) && daysToTimeStop!.Value >= 0 && daysToTimeStop.Value <= 7)
            {
                exitScore += 20;
                reasons.Add("time_stop_near");
            }

            if (IsValidNumber(profitPct) && profitPct!.Value >= 10)
            {
                exitScore += 15;
                reasons.Add("profit_protect_zone");
            }

            entryScore = Math.Max(0.0, Math.Min(100.0, Math.Round(entryScore)));
            exitScore = Math.Max(0.0, Math.Min(100.0, Math.Round(exitScore)));

            string action = "HOLD_NO_TIMING_EDGE";
            double? atr20 = GetNullableDouble(ctx, "atr20");
            string priceStatus = GetString(ctx, "priceStatus");

            if (priceStatus != "PRICE_OK" || !IsValidNumber(atr20))
            {
                action = "OBSERVE_DATA_MISSING";
            }
            else if (exitScore >= 75 || (IsValidNumber(rwPartial) && rwPartial!.Value >= 4))
            {
                action = "STOP_OR_TIME_EXIT_READY";
            }
            else if (exitScore >= 50 || (IsValidNumber(rwPartial) && rwPartial!.Value >= 3))
            {
                action = "EXIT_REVIEW";
            }
            else if (entryGate == "BLOCK" || acGate == "BLOCK" || entryMode == "OVERBOUGHT")
            {
                action = "NO_BUY_OVERHEATED";
            }
            else if (entryScore >= 75 && entryGate == "PASS" && leaderTotal.HasValue && leaderTotal.Value >= 4)
            {
                action = entryMode == "BREAKOUT" ? "BUY_BREAKOUT_PILOT_ONLY" : "BUY_STAGE1_READY";
            }
            else if (entryScore >= 60 && entryGate == "PASS")
            {
                action = entryMode == "BREAKOUT" ? "BUY_BREAKOUT_PILOT_ONLY" : "BUY_PULLBACK_WAIT";
            }
            else if ((leaderTotal.HasValue && leaderTotal.Value >= 3) || (flowCredit.HasValue && flowCredit.Value >= 0.4))
            {
                action = "WATCH_TIMING_SETUP";
            }

            // Slice down reasons to max 6 elements to align with Python output
            int takeCount = Math.Min(6, reasons.Count);
            string reasonStr = string.Join("|", reasons.GetRange(0, takeCount));

            return new TimingDecisionResult
            {
                EntryScore = entryScore,
                ExitScore = exitScore,
                Action = action,
                Reason = reasonStr
            };
        }

        public static SellDecisionResult ComputeSellDecision(Dictionary<string, object> ctx)
        {
            double? close = GetNullableDouble(ctx, "close");
            double? stopPrice = GetNullableDouble(ctx, "stopPrice");
            double? trailingStop = GetNullableDouble(ctx, "trailingStop");
            double? tp1Price = GetNullableDouble(ctx, "tp1Price");
            double? tp2Price = GetNullableDouble(ctx, "tp2Price");
            double? profitPct = GetNullableDouble(ctx, "profitPct");
            double? rwPartial = GetNullableDouble(ctx, "rwPartial");
            double? timingExitScore = GetNullableDouble(ctx, "timingExitScore");
            double? daysToTimeStop = GetNullableDouble(ctx, "daysToTimeStop");
            string timingAction = GetString(ctx, "timingAction");
            double? atr20 = GetNullableDouble(ctx, "atr20");

            double closeF = close ?? double.NaN;
            double stopF = stopPrice ?? double.NaN;
            double trailingF = trailingStop ?? double.NaN;
            double tp1F = tp1Price ?? double.NaN;
            double tp2F = tp2Price ?? double.NaN;
            double profitF = profitPct ?? double.NaN;
            double rwF = rwPartial ?? double.NaN;
            double timingExitF = timingExitScore ?? double.NaN;
            double daysF = daysToTimeStop ?? double.NaN;
            double atrF = atr20 ?? double.NaN;

            string action = "HOLD";
            double ratio = 0;
            string reason = "";
            double? price = null;
            string priceSource = "";
            string priceBasis = "";
            string executionWindow = "";
            string orderType = "";

            double stopCandidate = (double.IsNaN(trailingF) || trailingF <= 0) ? stopF : trailingF;
            if ((double.IsNaN(stopCandidate) || stopCandidate <= 0) && !double.IsNaN(closeF) && closeF > 0)
            {
                stopCandidate = closeF * 0.995;
            }

            double? protectiveLimit = null;
            if (!double.IsNaN(closeF) && closeF > 0)
            {
                double candidate = (double.IsNaN(stopCandidate) || stopCandidate <= 0) ? closeF * 0.995 : stopCandidate;
                protectiveLimit = Math.Round(Math.Min(closeF * 0.995, candidate));
            }

            double atrBuffer = (!double.IsNaN(atrF) && atrF > 0) ? atrF * 0.3 : (double.IsNaN(closeF) ? 0 : closeF * 0.005);
            double? closeProtectLimit = !double.IsNaN(closeF) && closeF > 0 ? (double?)Math.Round(closeF - atrBuffer) : null;

            if (timingAction == "STOP_OR_TIME_EXIT_READY" || (!double.IsNaN(rwF) && rwF >= 4))
            {
                action = "EXIT_100";
                ratio = 100;
                reason = (!double.IsNaN(rwF) && rwF >= 4) ? "RW_EXIT_STRONG" : "STOP_OR_TIME_EXIT_READY";
                price = protectiveLimit;
                priceSource = !double.IsNaN(trailingF) ? "TRAILING_STOP" : "STOP_OR_CLOSE";
                priceBasis = !double.IsNaN(trailingF) ? "TRAILING_STOP_TRIGGER" : "STOP_OR_CLOSE_PROTECT";
                executionWindow = "INTRADAY_ON_TRIGGER";
                orderType = "PROTECTIVE_LIMIT_SELL";
            }
            else if ((!double.IsNaN(rwF) && rwF >= 3) || (!double.IsNaN(timingExitF) && timingExitF >= 75))
            {
                action = "TRIM_70";
                ratio = 70;
                reason = (!double.IsNaN(rwF) && rwF >= 3) ? "RW_EXIT" : "TIMING_EXIT_SCORE";
                price = protectiveLimit;
                priceSource = "RISK_REDUCTION";
                priceBasis = "RISK_REDUCTION_CLOSE_PROTECT";
                executionWindow = "INTRADAY_AFTER_09_30";
                orderType = "PROTECTIVE_LIMIT_SELL";
            }
            else if (!double.IsNaN(trailingF) && trailingF > 0 && !double.IsNaN(closeF) && closeF <= trailingF)
            {
                action = "TRAILING_STOP_BREACH";
                ratio = 70;
                reason = "TRAILING_STOP_PRICE_BREACH";
                price = Math.Round(trailingF);
                priceSource = "TRAILING_STOP_PRICE";
                priceBasis = "TRAILING_STOP_TRIGGER";
                executionWindow = "INTRADAY_ON_TRIGGER";
                orderType = "PROTECTIVE_LIMIT_SELL";
            }
            else if ((!double.IsNaN(rwF) && rwF >= 2) || (!double.IsNaN(rwF) && rwF >= 1 && !double.IsNaN(timingExitF) && timingExitF >= 50))
            {
                action = "TRIM_50";
                ratio = 50;
                reason = (!double.IsNaN(rwF) && rwF >= 2) ? "RW_REVIEW" : "TIMING_EXIT_REVIEW";
                price = closeProtectLimit;
                priceSource = "RELATIVE_WEAKNESS_CLOSE";
                priceBasis = "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_AFTER_09_30";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(rwF) && rwF >= 1 && !double.IsNaN(timingExitF) && timingExitF >= 30)
            {
                action = "TRIM_33";
                ratio = 33;
                reason = "RW_EARLY_WARNING";
                price = closeProtectLimit;
                priceSource = "EARLY_WARNING_CLOSE";
                priceBasis = "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_AFTER_09_30";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(rwF) && rwF >= 1)
            {
                action = "TRIM_25";
                ratio = 25;
                reason = "RW_SIGNAL_ONLY";
                price = closeProtectLimit;
                priceSource = "SIGNAL_ONLY_CLOSE";
                priceBasis = "PRIOR_CLOSE_X_0.998";
                executionWindow = "CLOSE_REVIEW_OR_NEXT_OPEN";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(profitF) && profitF >= 50)
            {
                action = "PROFIT_TRIM_50";
                ratio = 50;
                reason = "PROFIT_PROTECT_50";
                price = (!double.IsNaN(tp2F) && tp2F > 0) ? (double?)Math.Round(tp2F) : closeProtectLimit;
                priceSource = !double.IsNaN(tp2F) ? "TP2_PRICE" : "CLOSE_PROFIT_PROTECT";
                priceBasis = !double.IsNaN(tp2F) ? "TAKE_PROFIT_TIER2_PRICE" : "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_LIMIT_OR_CLOSE_REVIEW";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(profitF) && profitF >= 30)
            {
                action = "PROFIT_TRIM_35";
                ratio = 35;
                reason = "PROFIT_PROTECT_30";
                price = (!double.IsNaN(tp2F) && tp2F > 0) ? (double?)Math.Round(tp2F) : closeProtectLimit;
                priceSource = !double.IsNaN(tp2F) ? "TP2_PRICE" : "CLOSE_PROFIT_PROTECT";
                priceBasis = !double.IsNaN(tp2F) ? "TAKE_PROFIT_TIER2_PRICE" : "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_LIMIT_OR_CLOSE_REVIEW";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(profitF) && profitF >= 20)
            {
                action = "PROFIT_TRIM_25";
                ratio = 25;
                reason = "PROFIT_PROTECT_20";
                price = (!double.IsNaN(tp1F) && tp1F > 0) ? (double?)Math.Round(tp1F) : closeProtectLimit;
                priceSource = !double.IsNaN(tp1F) ? "TP1_PRICE" : "CLOSE_PROFIT_PROTECT";
                priceBasis = !double.IsNaN(tp1F) ? "TAKE_PROFIT_TIER1_PRICE" : "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_LIMIT_OR_CLOSE_REVIEW";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(profitF) && profitF >= 10)
            {
                action = "TAKE_PROFIT_TIER1";
                ratio = 25;
                reason = "TP1_PROFIT_10PCT";
                price = (!double.IsNaN(tp1F) && tp1F > 0) ? (double?)Math.Round(tp1F) : closeProtectLimit;
                priceSource = !double.IsNaN(tp1F) ? "TP1_PRICE" : "CLOSE_PROFIT_PROTECT";
                priceBasis = !double.IsNaN(tp1F) ? "TAKE_PROFIT_TIER1_PRICE" : "PRIOR_CLOSE_X_0.998";
                executionWindow = "INTRADAY_LIMIT_OR_CLOSE_REVIEW";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(daysF) && daysF <= 0)
            {
                action = "TIME_EXIT_100";
                ratio = 100;
                reason = "TIME_STOP_EXPIRED";
                price = protectiveLimit;
                priceSource = "TIME_STOP_CLOSE";
                priceBasis = "TIME_STOP_CLOSE_PROTECT";
                executionWindow = "CLOSE_REVIEW_OR_NEXT_OPEN";
                orderType = "PROTECTIVE_LIMIT_SELL";
            }
            else if (!double.IsNaN(daysF) && daysF <= 7)
            {
                action = "TIME_TRIM_50";
                ratio = 50;
                reason = "TIME_STOP_NEAR";
                price = closeProtectLimit;
                priceSource = "TIME_STOP_NEAR_CLOSE";
                priceBasis = "ATR_PROTECT_LIMIT";
                executionWindow = "CLOSE_REVIEW_OR_NEXT_OPEN";
                orderType = "LIMIT_SELL";
            }
            else if (!double.IsNaN(daysF) && daysF <= 14)
            {
                action = "TIME_TRIM_25";
                ratio = 25;
                reason = "TIME_STOP_APPROACHING";
                price = closeProtectLimit;
                priceSource = "TIME_STOP_APPROACHING_CLOSE";
                priceBasis = "ATR_PROTECT_LIMIT";
                executionWindow = "CLOSE_REVIEW_OR_NEXT_OPEN";
                orderType = "LIMIT_SELL";
            }

            string validation = "NO_SELL_ACTION";
            if (action != "HOLD")
            {
                validation = (price.HasValue && price.Value > 0) ? "SIGNAL_CONFIRMED" : "NO_SELL_PRICE";
            }

            return new SellDecisionResult
            {
                Action = action,
                RatioPct = ratio,
                LimitPrice = price,
                PriceSource = priceSource,
                PriceBasis = priceBasis,
                ExecutionWindow = executionWindow,
                OrderType = orderType,
                Reason = reason,
                Validation = validation
            };
        }

        public static FinalDecisionResult ComputeFinalDecision(Dictionary<string, object> ctx)
        {
            string sellAction = GetString(ctx, "sellAction");
            if (string.IsNullOrEmpty(sellAction)) sellAction = "HOLD";

            string sellValidation = GetString(ctx, "sellValidation");
            string allowedAction = GetString(ctx, "allowedAction");
            string timingAction = GetString(ctx, "timingAction");
            double? timingEntry = GetNullableDouble(ctx, "timingScoreEntry");
            double? timingExit = GetNullableDouble(ctx, "timingScoreExit");
            double? ss001Total = GetNullableDouble(ctx, "ss001Total");
            double? flowCredit = GetNullableDouble(ctx, "flowCredit");
            double? leaderTotal = GetNullableDouble(ctx, "leaderTotal");
            double? rwPartial = GetNullableDouble(ctx, "rwPartial");
            double? profitPct = GetNullableDouble(ctx, "profitPct");
            double? daysToTimeStop = GetNullableDouble(ctx, "daysToTimeStop");
            double? weightPct = GetNullableDouble(ctx, "weightPct");
            string acGate = GetString(ctx, "acGate");
            string liquidityStatus = GetString(ctx, "liquidityStatus");
            string spreadStatus = GetString(ctx, "spreadStatus");
            bool dartRisk = GetBool(ctx, "dartRisk");
            string missingFields = GetString(ctx, "missingFields");

            string finalAction = "HOLD";
            int actionPriority = 99;
            string decisionSource = "RULE_ENGINE";

            if (sellAction != "HOLD" && sellValidation == "SIGNAL_CONFIRMED")
            {
                finalAction = "SELL_READY";
                actionPriority = 10;
            }
            else if (allowedAction == "EXIT_SIGNAL" || timingAction == "STOP_OR_TIME_EXIT_READY")
            {
                finalAction = "EXIT_SIGNAL";
                actionPriority = 28;
            }
            else if (allowedAction == "REVIEW_EXIT" || timingAction == "EXIT_REVIEW")
            {
                finalAction = "EXIT_REVIEW";
                actionPriority = 32;
            }
            else if (timingAction == "NO_BUY_OVERHEATED" && !dartRisk)
            {
                finalAction = "NO_BUY_OVERHEATED";
                actionPriority = 50;
            }
            else if (allowedAction == "BUY_STAGE1_READY" || timingAction == "BUY_STAGE1_READY")
            {
                finalAction = "BUY_STAGE1_READY";
                actionPriority = 60;
            }
            else if (allowedAction == "BUY_BREAKOUT_PILOT_ONLY" || timingAction == "BUY_BREAKOUT_PILOT_ONLY")
            {
                finalAction = "BUY_BREAKOUT_PILOT_ONLY";
                actionPriority = 70;
            }
            else if (allowedAction == "BUY_PULLBACK_WAIT" || timingAction == "BUY_PULLBACK_WAIT")
            {
                finalAction = "BUY_PULLBACK_WAIT";
                actionPriority = 80;
            }
            else if (allowedAction == "WATCH_CANDIDATE")
            {
                finalAction = "WATCH_TIMING_SETUP";
                actionPriority = 90;
            }

            if (!string.IsNullOrEmpty(missingFields))
            {
                decisionSource = "RULE_ENGINE_WITH_MISSING_DATA";
            }

            double timeStopUrgency = (daysToTimeStop.HasValue && !double.IsNaN(daysToTimeStop.Value) && daysToTimeStop.Value >= 0) 
                ? Math.Max(0.0, 20.0 - Math.Min(20.0, daysToTimeStop.Value * 3.0)) 
                : 0.0;
            double overweightPenalty = (weightPct.HasValue && !double.IsNaN(weightPct.Value) && weightPct.Value > 7) ? 15.0 : 0.0;
            double overheatPenalty = acGate == "BLOCK" ? 30.0 : (acGate == "CAUTION" ? 10.0 : 0.0);
            double liquidityPenalty = (liquidityStatus == "LOW" || liquidityStatus == "DATA_MISSING" || spreadStatus == "BLOCK" || spreadStatus == "WIDE" || spreadStatus == "QUOTE_NO_MATCH") ? 15.0 : 0.0;

            double priorityScore = 0.0;
            if (actionPriority <= 40)
            {
                double exitVal = timingExit ?? 0.0;
                double rwVal = rwPartial ?? 0.0;
                double profitVal = profitPct ?? 0.0;
                priorityScore = exitVal * 0.35 + rwVal * 15.0 + Math.Max(0.0, profitVal) * 0.30 + timeStopUrgency + overweightPenalty;
            }
            else if (actionPriority >= 50 && actionPriority <= 80)
            {
                double entryVal = timingEntry ?? 0.0;
                double ssVal = ss001Total ?? 0.0;
                double flowVal = flowCredit ?? 0.0;
                double leaderVal = leaderTotal ?? 0.0;
                priorityScore = entryVal * 0.35 + ssVal * 0.30 + flowVal * 20.0 + leaderVal * 5.0 - overheatPenalty - liquidityPenalty;
            }
            else
            {
                double entryVal = timingEntry ?? 0.0;
                double exitVal = timingExit ?? 0.0;
                double flowVal = flowCredit ?? 0.0;
                priorityScore = entryVal * 0.20 + exitVal * 0.20 + flowVal * 10.0;
            }

            return new FinalDecisionResult
            {
                FinalAction = finalAction,
                ActionPriority = actionPriority,
                PriorityScore = Math.Max(0.0, priorityScore),
                DecisionSource = decisionSource
            };
        }

        public static CashShortfallResult ComputeCashShortfallHarness(
            Dictionary<string, object> asResult,
            double totalAsset,
            Dictionary<string, object> cashFloorInfo,
            double mrsScore)
        {
            double asset = (double.IsFinite(totalAsset) && totalAsset > 0) ? totalAsset : 0.0;
            double d2Krw = 0.0;
            if (asResult.TryGetValue("settlementCashD2Krw", out var d2Val) && double.TryParse(d2Val?.ToString(), out var d2d))
            {
                d2Krw = d2d;
            }
            double minPct = 0.0;
            if (cashFloorInfo.TryGetValue("minPct", out var minVal) && double.TryParse(minVal?.ToString(), out var minPctD))
            {
                minPct = minPctD;
            }

            double targetCashPct = Math.Max(5.0 + (mrsScore / 10.0) * 15.0, minPct);

            return new CashShortfallResult
            {
                CashCurrentPctD2 = asset > 0 ? Math.Round((d2Krw / asset * 100.0), 2) : 0.0,
                CashTargetPct = targetCashPct,
                CashShortfallMinKrw = Math.Max(0.0, Math.Round(asset * minPct / 100.0 - d2Krw)),
                CashShortfallTargetKrw = Math.Max(0.0, Math.Round(asset * targetCashPct / 100.0 - d2Krw))
            };
        }

        public static CashRecoveryPlanResult ComputeCashRecoveryOptimizer(
            List<Dictionary<string, object>> sellCandidates,
            double cashShortfallMinKrw)
        {
            var plan = new List<CashRecoveryPlanItem>();
            double cumulativeKrw = 0.0;

            foreach (var cand in sellCandidates)
            {
                if (cumulativeKrw >= cashShortfallMinKrw)
                {
                    break;
                }

                string ticker = GetString(cand, "Ticker") ?? GetString(cand, "ticker") ?? "";
                string name = GetString(cand, "Name") ?? GetString(cand, "name") ?? "";
                
                int qty = 0;
                if (cand.TryGetValue("Sell_Qty", out var qtyVal) && int.TryParse(qtyVal?.ToString(), out var qtyI))
                {
                    qty = qtyI;
                }

                double limitPrice = 0.0;
                if (cand.TryGetValue("Sell_Limit_Price", out var lpVal) && double.TryParse(lpVal?.ToString(), out var lpD))
                {
                    limitPrice = lpD;
                }
                else if (cand.TryGetValue("current_price", out var cpVal) && double.TryParse(cpVal?.ToString(), out var cpD))
                {
                    limitPrice = cpD;
                }

                double preserveRatio = 100.0;
                if (cand.TryGetValue("Cash_Preserve_Ratio", out var prVal) && double.TryParse(prVal?.ToString(), out var prD))
                {
                    preserveRatio = prD;
                }

                string style = GetString(cand, "Cash_Preserve_Style") ?? "FULL";

                double expectedKrw = 0.0;
                if (qty > 0 && limitPrice > 0)
                {
                    expectedKrw = qty * limitPrice * (preserveRatio / 100.0);
                }

                plan.Add(new CashRecoveryPlanItem
                {
                    Ticker = ticker,
                    Name = name,
                    Qty = qty,
                    LimitPrice = limitPrice > 0 ? (double?)KrxTickNormalizer.NormalizeTick(limitPrice) : null,
                    PreserveStyle = style,
                    PreserveRatio = preserveRatio,
                    ExpectedKrw = Math.Round(expectedKrw)
                });

                cumulativeKrw += expectedKrw;
            }

            bool shortfallMet = cumulativeKrw >= cashShortfallMinKrw;

            return new CashRecoveryPlanResult
            {
                SellSequence = plan,
                ExpectedTotalKrw = Math.Round(cumulativeKrw),
                CashShortfallMinKrw = cashShortfallMinKrw,
                ShortfallMet = shortfallMet,
                ItemsNeeded = plan.Count
            };
        }

        // Helpers
        private static string GetString(Dictionary<string, object> dict, string key)
        {
            if (dict.TryGetValue(key, out var val) && val != null)
            {
                return val.ToString() ?? "";
            }
            return "";
        }

        private static double? GetNullableDouble(Dictionary<string, object> dict, string key)
        {
            if (dict.TryGetValue(key, out var val) && val != null)
            {
                if (double.TryParse(val.ToString(), out var d))
                {
                    return d;
                }
            }
            return null;
        }

        private static bool GetBool(Dictionary<string, object> dict, string key)
        {
            if (dict.TryGetValue(key, out var val) && val != null)
            {
                if (bool.TryParse(val.ToString(), out var b))
                {
                    return b;
                }
                if (double.TryParse(val.ToString(), out var d))
                {
                    return d != 0;
                }
            }
            return false;
        }
    }
}
