using ConsoleTableExt;
using LibraryPricer.Payoffs;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace LibraryPricer
{
    public static class FairValueComparison
    {
        public static PayoffContext CloneContext(PayoffContext original)
        {
            return new PayoffContext
            {
                DaysToMaturity = original.DaysToMaturity,
                r = original.r,
                CapitalValue = original.CapitalValue,
                ProtMinVal = original.ProtMinVal,
                PDI_Barrier = original.PDI_Barrier,
                PDI_Style = original.PDI_Style,
                CouponInMemory = original.CouponInMemory,
                DaysToObsYFract = (double[])original.DaysToObsYFract.Clone(),
                CouponTriggers = (double[])original.CouponTriggers.Clone(),
                CouponValues = (double[])original.CouponValues.Clone(),
                AutocallTriggers = (double[])original.AutocallTriggers.Clone(),
                AutocallValues = (double[])original.AutocallValues.Clone(),
                MemoryFlags = (int[])original.MemoryFlags.Clone(),
                Caso = original.Caso,
                PDI_Strike = original.PDI_Strike,
                FattoreAirbag = original.FattoreAirbag,
                TriggerOneStar = original.TriggerOneStar,
                CAP = original.CAP,
                DaysToObs = (int[])original.DaysToObs.Clone(),
                Airbag = original.Airbag,
                Sigma = original.Sigma,
                Relief = original.Relief,
                TwinWin = original.TwinWin,
                OneStar = original.OneStar,
                Leva = original.Leva,
                Volatility = (double[])original.Volatility.Clone(),
                PricesUL = (double[])original.PricesUL.Clone(),
                NominalAmount = original.NominalAmount
            };
        }

        public static void CompareFairValues(
            int numSimulations,
            double[][] pricesUL,
            double[,] corrMatrix,
            int numAssets,
            int daysToMaturity,
            double r,
            double[] dividends,
            PayoffContext context,
            double nominalAmount,
            string isin,
            double ask,
            double bid,
            DateTime lastDate,
            string category,
            DateTime[]? obsDates = null)
        {
            Console.WriteLine($"Motore GBM con payoff engine '{context.Caso}'");

            var payoffEvaluator = PayoffFactory.GetEvaluator(context.Caso);
            double[] results = new double[numSimulations];
            int barrierBreaksAtMaturity = 0;
            int autocallFirstObsIndex = -1;
            double autocallFirstTrigger = 0.0;
            int autocallAtFirstObsCount = 0;
            int oneStarObsIndex = -1;
            int oneStarTriggerHits = 0;

            if (context.AutocallTriggers != null && context.DaysToObs != null)
            {
                for (int k = 0; k < context.AutocallTriggers.Length && k < context.DaysToObs.Length; k++)
                {
                    double trigger = context.AutocallTriggers[k];
                    if (trigger != 999 && trigger != 0)
                    {
                        autocallFirstObsIndex = k;
                        autocallFirstTrigger = trigger;
                        break;
                    }
                }
            }

            if (autocallFirstObsIndex < 0 && context.AutocallTriggers != null && context.DaysToObs != null && context.AutocallValues != null)
            {
                for (int k = 0; k < context.AutocallTriggers.Length && k < context.DaysToObs.Length && k < context.AutocallValues.Length; k++)
                {
                    double trigger = context.AutocallTriggers[k];
                    if (trigger != 999 && context.AutocallValues[k] > 0)
                    {
                        autocallFirstObsIndex = k;
                        autocallFirstTrigger = trigger;
                        break;
                    }
                }
            }

            if (context.OneStar == 1 && context.DaysToObs != null && context.DaysToObs.Length > 0 && context.TriggerOneStar > 0)
            {
                oneStarObsIndex = context.DaysToObs.Length - 1;
            }

            for (int i = 0; i < numSimulations; i++)
            {
                try
                {
                    var path = CalcFunctions.GBMMultiEquity(
                        pricesUL[i],
                        corrMatrix,
                        numAssets,
                        daysToMaturity,
                        r,
                        dividends,
                        context.Volatility
                    );

                    if (context.PDI_Barrier > 0)
                    {
                        double minAtMaturity = CalcFunctions.MinArrayAtRow(path, numAssets, daysToMaturity);
                        if (minAtMaturity < context.PDI_Barrier)
                        {
                            barrierBreaksAtMaturity++;
                        }
                    }

                    if (autocallFirstObsIndex >= 0)
                    {
                        int obsDay = context.DaysToObs[autocallFirstObsIndex];
                        double minAtObs = CalcFunctions.MinArrayAtRow(path, numAssets, obsDay);
                        if (minAtObs >= autocallFirstTrigger)
                        {
                            autocallAtFirstObsCount++;
                        }
                    }

                    if (oneStarObsIndex >= 0)
                    {
                        int obsDay = context.DaysToObs[oneStarObsIndex];
                        double maxAtObs = CalcFunctions.MaxArrayAtRow(path, numAssets, obsDay);
                        if (maxAtObs >= context.TriggerOneStar)
                        {
                            oneStarTriggerHits++;
                        }
                    }

                    var payoff = payoffEvaluator.Evaluate(path, context);
                    results[i] = payoff * 100.0;
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"[SIM {i}] Errore: {ex.Message}");
                    results[i] = 0.0;
                }
            }

            double sum = results.Sum();
            double mean = sum / numSimulations;
            double variance = results.Select(val => Math.Pow(val - mean, 2)).Sum() / numSimulations;
            double stddev = Math.Sqrt(variance);

            double fairValueScaled = mean * nominalAmount / 100.0;
            int[] fairValues = results.Select(x => (int)Math.Floor(x * nominalAmount / 100)).ToArray();

            // Report sintetico in console (senza dipendenze esterne)
            Console.WriteLine($"Caso calcolo Fair value {isin} = {category}");
            Console.WriteLine($"Fair value {isin} ({numSimulations} iter.) = {fairValueScaled:F2} (Bid = {bid:F2}, Ask = {ask:F2}, Last updated on {lastDate:dd/MM/yyyy HH:mm:ss})");

            if (context.PDI_Barrier > 0)
            {
                double pctBarrierBreak = 100.0 * barrierBreaksAtMaturity / Math.Max(1, numSimulations);
                Console.WriteLine($"Probabilita rottura barriera a scadenza: {pctBarrierBreak:F2}% (Barriera {context.PDI_Barrier:F4})");
            }

            if (autocallFirstObsIndex >= 0)
            {
                double pctAutocallFirstObs = 100.0 * autocallAtFirstObsCount / Math.Max(1, numSimulations);
                string obsLabel = (obsDates != null && autocallFirstObsIndex < obsDates.Length)
                    ? obsDates[autocallFirstObsIndex].ToString("dd/MM/yyyy")
                    : context.DaysToObs[autocallFirstObsIndex].ToString();
                Console.WriteLine($"Probabilita rimborso anticipato alla prima osservazione autocall: {pctAutocallFirstObs:F2}% (Trigger {autocallFirstTrigger:F4}, Obs {obsLabel})");
            }
            else
            {
                Console.WriteLine("Probabilita rimborso anticipato alla prima osservazione autocall: N/D (nessun trigger autocall valorizzato)");
            }

            if (oneStarObsIndex >= 0)
            {
                double pctOneStar = 100.0 * oneStarTriggerHits / Math.Max(1, numSimulations);
                string obsLabel = (obsDates != null && oneStarObsIndex < obsDates.Length)
                    ? obsDates[oneStarObsIndex].ToString("dd/MM/yyyy")
                    : context.DaysToObs[oneStarObsIndex].ToString();
                Console.WriteLine($"Probabilita attivazione OneStar: {pctOneStar:F2}% (Trigger {context.TriggerOneStar:F4}, Obs {obsLabel})");
            }

            double referencePrice = ask > 0 ? ask : (bid > 0 ? bid : 0.0);
            string referenceLabel = ask > 0 ? "Ask" : "Bid";

            if (referencePrice > 0)
            {
                double[] above = fairValues.Where(v => v >= referencePrice).Select(v => (double)v).ToArray();
                double[] below = fairValues.Where(v => v < referencePrice).Select(v => (double)v).ToArray();

                double pctAbove = 100.0 * above.Length / Math.Max(1, fairValues.Length);
                double pctBelow = 100.0 * below.Length / Math.Max(1, fairValues.Length);
                Console.WriteLine($"Sopra {referenceLabel} nel {pctAbove:F2}% dei casi, sotto {referenceLabel} nel {pctBelow:F2}% dei casi");

                double avgAbove = above.Length > 0 ? above.Average() : 0.0;
                double avgBelow = below.Length > 0 ? below.Average() : 0.0;
                Console.WriteLine($"Valore medio casi sopra {referenceLabel}: {avgAbove:F2}");
                Console.WriteLine($"Valore medio casi sotto {referenceLabel}: {avgBelow:F2}");

                double gainAvg = avgAbove - referencePrice;
                double lossAvg = referencePrice - avgBelow;
                Console.WriteLine($"Gain medio: {avgAbove:F2} - {referencePrice:F2} = {gainAvg:F2}");
                Console.WriteLine($"Loss medio: {referencePrice:F2} - {avgBelow:F2} = {lossAvg:F2}");

                double pctGain = referencePrice > 0 ? 100.0 * gainAvg / referencePrice : 0.0;
                double pctLoss = referencePrice > 0 ? 100.0 * lossAvg / referencePrice : 0.0;
                Console.WriteLine($"Perc media Gain: {gainAvg:F2} / {referencePrice:F2} = {pctGain:F2}%");
                Console.WriteLine($"Perc media Loss: {lossAvg:F2} / {referencePrice:F2} = {pctLoss:F2}%");
                double expectedValue = (gainAvg * (pctAbove / 100.0)) - (lossAvg * (pctBelow / 100.0));
                Console.WriteLine($"Expected value: {expectedValue:F2}");

                double[] returnsPct = fairValues.Select(v => 100.0 * (v - referencePrice) / referencePrice).ToArray();
                if (returnsPct.Length > 0)
                {
                    double minRet = returnsPct.Min();
                    double maxRet = returnsPct.Max();
                    if (Math.Abs(maxRet - minRet) < 1e-9)
                    {
                        Console.WriteLine($"Distribuzione rendimenti vs {referenceLabel}: tutti i casi {returnsPct[0]:F2}%");
                    }
                    else
                    {
                        Console.WriteLine($"Distribuzione rendimenti vs {referenceLabel} (10 bins):");
                        int bins = 10;
                        double width = (maxRet - minRet) / bins;
                        int[] counts = new int[bins];

                        foreach (double ret in returnsPct)
                        {
                            int idx = (int)Math.Floor((ret - minRet) / width);
                            if (idx >= bins) idx = bins - 1;
                            if (idx < 0) idx = 0;
                            counts[idx]++;
                        }

                        int maxCount = counts.Max();
                        for (int b = 0; b < bins; b++)
                        {
                            double start = minRet + b * width;
                            double end = (b == bins - 1) ? maxRet : (start + width);
                            int count = counts[b];
                            double pct = 100.0 * count / Math.Max(1, returnsPct.Length);
                            int barLen = maxCount > 0 ? (int)Math.Round(50.0 * count / maxCount) : 0;
                            string bar = new string('#', barLen);
                            Console.WriteLine($"[{start,7:F2}% .. {end,7:F2}%] {count,5} ({pct,5:F2}%) | {bar}");
                        }

                        string safeLabel = referenceLabel.Replace(" ", "");
                        string fileName = $"returns_hist_{isin}_{safeLabel}.png";
                        string outDir = Path.Combine(GetProjectRoot(), "output");
                        Directory.CreateDirectory(outDir);
                        string outPath = Path.Combine(outDir, fileName);
                        WriteHistogramPng(outPath, minRet, maxRet, counts, referenceLabel, isin);
                        Console.WriteLine($"Plot salvato: {outPath}");
                    }
                }
            }

        }

        private static void WriteHistogramPng(string path, double minRet, double maxRet, int[] counts, string referenceLabel, string isin)
        {
            const int width = 900;
            const int height = 520;
            const int marginLeft = 70;
            const int marginRight = 30;
            const int marginTop = 50;
            const int marginBottom = 90;

            int bins = counts.Length;
            int maxCount = counts.Max();
            double plotWidth = width - marginLeft - marginRight;
            double plotHeight = height - marginTop - marginBottom;
            double barWidth = plotWidth / Math.Max(1, bins);
            double range = Math.Max(1e-9, maxRet - minRet);

            using var bmp = new Bitmap(width, height);
            using var g = Graphics.FromImage(bmp);
            g.Clear(Color.White);
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

            using var axisPen = new Pen(Color.FromArgb(51, 51, 51), 1);
            using var textBrush = new SolidBrush(Color.FromArgb(51, 51, 51));
            using var titleBrush = new SolidBrush(Color.FromArgb(17, 17, 17));
            using var blueBrush = new SolidBrush(Color.FromArgb(74, 123, 209));
            using var redBrush = new SolidBrush(Color.FromArgb(209, 100, 74));
            using var fontTitle = new Font("Segoe UI", 14);
            using var fontLabel = new Font("Segoe UI", 10);
            using var fontTick = new Font("Segoe UI", 9);

            g.DrawString($"Distribuzione rendimenti vs {referenceLabel} - {isin}", fontTitle, titleBrush, marginLeft, 20);

            // Axes
            g.DrawLine(axisPen, marginLeft, marginTop, marginLeft, height - marginBottom);
            g.DrawLine(axisPen, marginLeft, height - marginBottom, width - marginRight, height - marginBottom);

            // Bars
            for (int i = 0; i < bins; i++)
            {
                double ratio = maxCount > 0 ? (double)counts[i] / maxCount : 0.0;
                double barHeight = ratio * plotHeight;
                double x = marginLeft + i * barWidth + 2;
                double y = (height - marginBottom) - barHeight;
                double bw = Math.Max(1, barWidth - 4);
                double binCenter = minRet + (i + 0.5) * (range / bins);
                var brush = binCenter < 0 ? redBrush : blueBrush;
                g.FillRectangle(brush, (float)x, (float)y, (float)bw, (float)barHeight);
            }

            // X-axis labels (min, 0 if in range, max)
            g.DrawString($"{minRet:F2}%", fontLabel, textBrush, marginLeft, height - marginBottom + 35);
            if (minRet < 0 && maxRet > 0)
            {
                double xZero = marginLeft + ((0 - minRet) / range) * plotWidth;
                using var dashPen = new Pen(Color.FromArgb(153, 153, 153), 1);
                dashPen.DashPattern = new float[] { 4, 3 };
                g.DrawLine(dashPen, (float)xZero, marginTop, (float)xZero, height - marginBottom);
                g.DrawString("0%", fontLabel, textBrush, (float)(xZero - 10), height - marginBottom + 35);
            }
            g.DrawString($"{maxRet:F2}%", fontLabel, textBrush, width - marginRight - 60, height - marginBottom + 35);
            g.DrawString("Rendimento (%)", fontLabel, textBrush, width - marginRight - 140, height - 25);

            // Y-axis labels (0 and max)
            g.DrawString("0", fontLabel, textBrush, marginLeft - 25, height - marginBottom - 5);
            g.DrawString($"{maxCount}", fontLabel, textBrush, marginLeft - 35, marginTop - 5);
            g.DrawString("Conteggi", fontLabel, textBrush, marginLeft - 55, marginTop - 25);

            // Bin labels
            using var centerFormat = new StringFormat { Alignment = StringAlignment.Center };
            for (int i = 0; i < bins; i++)
            {
                double start = minRet + i * (range / bins);
                double end = (i == bins - 1) ? maxRet : (start + (range / bins));
                string label = $"{start:F1}..{end:F1}%";
                float x = (float)(marginLeft + i * barWidth + barWidth / 2);
                float y = height - marginBottom + 55;
                g.DrawString(label, fontTick, textBrush, x, y, centerFormat);
            }

            bmp.Save(path, ImageFormat.Png);
        }

        private static string GetProjectRoot()
        {
            DirectoryInfo? dir = new DirectoryInfo(Directory.GetCurrentDirectory());
            while (dir != null)
            {
                bool hasSln = dir.EnumerateFiles("*.sln").Any();
                bool hasPricer = Directory.Exists(Path.Combine(dir.FullName, "Pricer"));
                bool hasLibrary = Directory.Exists(Path.Combine(dir.FullName, "LibraryPricer"));
                if (hasSln || (hasPricer && hasLibrary))
                {
                    return dir.FullName;
                }
                dir = dir.Parent;
            }
            return Directory.GetCurrentDirectory();
        }
    }
}
