using LiteDB;
using Microsoft.Win32;
using NSwag;
using NSwag.CodeGeneration.CSharp;
using Nodify.Calculator.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Windows;

namespace Nodify.Calculator
{
    public class OperationsMenuViewModel : ObservableObject
    {
        private bool _isVisible;
        public bool IsVisible
        {
            get => _isVisible;
            set
            {
                SetProperty(ref _isVisible, value);
                if (!value)
                {
                    Closed?.Invoke();
                }
            }
        }

        private Point _location;
        public Point Location
        {
            get => _location;
            set => SetProperty(ref _location, value);
        }

        public event Action? Closed;

        public void OpenAt(Point targetLocation)
        {
            MenuAvailableOperations.Clear();
            MenuAvailableOperations.AddRange(AvailableOperations);
            Close();
            Location = targetLocation;
            IsVisible = true;
        }
        
        public void OpenOnlyGetSetVariable(Point targetLocation, string className)
        {
            var getVM = new OperationInfoViewModel()
            {
                Title = "GET",
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                IsModelNode = true,
                ClassName = className,
            };
            var setVM = new OperationInfoViewModel()
            {
                Title = "SET",
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                IsModelNode = true,
                ClassName = className
            };
            MenuAvailableOperations.Clear();
            MenuAvailableOperations.Add(getVM);
            MenuAvailableOperations.Add(setVM);
            Close();
            Location = targetLocation;
            IsVisible = true;
        }

        public void OpenGetSetForVariable(Point targetLocation, OperationInfoViewModel variableInfo)
        {
            var getVM = new OperationInfoViewModel()
            {
                Title = "GET",
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                IsSimpleVariable = true,
                VariableType = variableInfo.VariableType,
                DefaultValue = variableInfo.DefaultValue,
                ClassName = variableInfo.Title ?? string.Empty
            };
            var setVM = new OperationInfoViewModel()
            {
                Title = "SET",
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                IsSimpleVariable = true,
                VariableType = variableInfo.VariableType,
                DefaultValue = variableInfo.DefaultValue,
                ClassName = variableInfo.Title ?? string.Empty
            };
            MenuAvailableOperations.Clear();
            MenuAvailableOperations.Add(getVM);
            MenuAvailableOperations.Add(setVM);
            Close();
            Location = targetLocation;
            IsVisible = true;
        }


        public void Close()
        {
            IsVisible = false;
        }

        public NodifyObservableCollection<OperationInfoViewModel> MenuAvailableOperations { get; }
        public NodifyObservableCollection<OperationInfoViewModel> AvailableOperations { get; }
        public NodifyObservableCollection<OperationInfoViewModel> SwaggerOperations { get; }
        public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
        public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
        public NodifyObservableCollection<OperationInfoViewModel> AvailableFunctions { get; }

        /// <summary>Swagger operations grouped by SwaggerGroup (tag), for the right-side panel.</summary>
        public NodifyObservableCollection<SwaggerGroupViewModel> GroupedSwaggerOperations { get; } = new NodifyObservableCollection<SwaggerGroupViewModel>();

        /// <summary>Non-API system nodes for the right-side panel.</summary>
        public NodifyObservableCollection<OperationInfoViewModel> SystemNodes { get; } = new NodifyObservableCollection<OperationInfoViewModel>();
        public INodifyCommand CreateOperationCommand { get; }

        [Newtonsoft.Json.JsonIgnore]
        [BsonIgnore]
        private readonly CalculatorViewModel _calculator;

        public OperationsMenuViewModel(CalculatorViewModel calculator)
        {
            _calculator = calculator;
            List<OperationInfoViewModel> operations = new List<OperationInfoViewModel>();
            AvailableModels = new NodifyObservableCollection<OperationInfoViewModel>();
            AvailableVariables = new NodifyObservableCollection<OperationInfoViewModel>();
            AvailableFunctions = new NodifyObservableCollection<OperationInfoViewModel>();
            LoadVariablesFromDb();

            var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
            Directory.CreateDirectory(customModelDir);
            var dirInfo = new DirectoryInfo(customModelDir);
            var allFiles = dirInfo.GetFiles("*.cs");
            foreach (var item in allFiles)
            {
                var flName = Path.GetFileNameWithoutExtension(item.Name);
                var opVInfo = new OperationInfoViewModel()
                {
                    Title = flName,
                    IsModelNode = true,
                    Type = OperationType.System,
                    sysOp = SystemOperations.GET_SET,
                    ClassName = flName
                };
                AvailableModels.Add(opVInfo);
            }

            operations.AddRange(OperationFactory.GetSystemNodes());
            //operations.AddRange(OperationFactory.GetOperationsInfo(typeof(OperationsContainer)));

            // Populate system-only nodes for the right panel
            foreach (var sysNode in operations)
                SystemNodes.Add(sysNode);

            SwaggerOperations = new NodifyObservableCollection<OperationInfoViewModel>();
            LoadSwaggerNodesFromDb();
            RebuildGroupedSwaggerOperations();

            operations.AddRange(SwaggerOperations);

            AvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
            MenuAvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
            CreateOperationCommand = new DelegateCommand<OperationInfoViewModel>(CreateOperation);
            ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
            AddVariableCommand = new DelegateCommand(AddVariable);
            CreateFunctionCommand = new DelegateCommand(CreateFunction);
            CreateModelCommand = new DelegateCommand(CreateModel);
        }

        
        public void AddNewModel(OperationInfoViewModel opModel)
        {
            if (System.Windows.Application.Current.Dispatcher.CheckAccess())
            {
                // Add directly if on UI thread
                AvailableModels.Add(opModel);
            }
            else
            {
                // Otherwise, marshal the call to the UI thread
                System.Windows.Application.Current.Dispatcher.Invoke(() =>
                {
                    AvailableModels.Add(opModel);
                });
            }
        }

        public INodifyCommand ImportSwaggerCommand { get; }
        public INodifyCommand AddVariableCommand { get; }
        public INodifyCommand CreateFunctionCommand { get; }
        public INodifyCommand CreateModelCommand { get; }

        private void CreateModel()
        {
            var dialog = new CreateModelDialog();
            dialog.Owner = System.Windows.Application.Current.MainWindow;
            if (dialog.ShowDialog() != true)
                return;

            var className = dialog.ModelName;

            if (AvailableModels.Any(m => m.Title == className))
            {
                MessageBox.Show($"A model named '{className}' already exists.", "Duplicate Model", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            // Generate .cs file
            var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
            Directory.CreateDirectory(customModelDir);
            var sb = new System.Text.StringBuilder();
            sb.AppendLine($"public class {className}");
            sb.AppendLine("{");
            foreach (var prop in dialog.Properties)
            {
                sb.AppendLine($"\tpublic {prop.Type} {prop.Name} {{ get; set; }}");
            }
            sb.AppendLine("}");
            File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString());

            var modelInfo = new OperationInfoViewModel
            {
                Title = className,
                IsModelNode = true,
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                ClassName = className
            };
            AvailableModels.Add(modelInfo);
        }

        private void CreateFunction()
        {
            var dialog = new CreateFunctionDialog();
            dialog.Owner = System.Windows.Application.Current.MainWindow;
            if (dialog.ShowDialog() != true)
                return;

            var funcName = dialog.FunctionName;

            if (AvailableFunctions.Any(f => f.Title == funcName))
            {
                MessageBox.Show($"A function named '{funcName}' already exists.", "Duplicate Function", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            var funcInfo = new OperationInfoViewModel
            {
                Title = funcName,
                Type = OperationType.System,
                sysOp = SystemOperations.FUNCTION,
                IsFlowNode = true,
                IsFunction = true,
                FunctionInputs = new System.Collections.Generic.List<FunctionParameterInfo>(dialog.InputParameters),
                FunctionOutputs = new System.Collections.Generic.List<FunctionParameterInfo>(dialog.OutputParameters)
            };

            AvailableFunctions.Add(funcInfo);
            AvailableOperations.Add(funcInfo);
        }

        private void AddVariable()
        {
            var dialog = new AddVariableDialog();
            dialog.Owner = System.Windows.Application.Current.MainWindow;
            if (dialog.ShowDialog() != true)
                return;

            var varName = dialog.VariableName;
            var varType = dialog.VariableType;
            var defaultVal = dialog.DefaultValue;

            // Check for duplicate name
            if (AvailableVariables.Any(v => v.Title == varName))
            {
                MessageBox.Show($"A variable named '{varName}' already exists.", "Duplicate Variable", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }

            var varInfo = CreateVariableInfo(varName, varType, defaultVal);
            AvailableVariables.Add(varInfo);
            SaveVariableToDb(varName, varType, defaultVal);
        }

        private OperationInfoViewModel CreateVariableInfo(string name, string varType, string defaultValue)
        {
            return new OperationInfoViewModel
            {
                Title = name,
                Type = OperationType.System,
                sysOp = SystemOperations.GET_SET,
                IsSimpleVariable = true,
                VariableType = varType,
                DefaultValue = defaultValue,
                IsModelNode = false
            };
        }

        private void SaveVariableToDb(string name, string varType, string defaultValue)
        {
            using var db = new LiteDbHelper<VariableModel>("Variables");
            db.Insert(new VariableModel
            {
                Name = name,
                VariableType = varType,
                DefaultValue = defaultValue
            });
        }

        private void LoadVariablesFromDb()
        {
            try
            {
                using var db = new LiteDbHelper<VariableModel>("Variables");
                foreach (var v in db.FindAll())
                {
                    AvailableVariables.Add(CreateVariableInfo(v.Name, v.VariableType, v.DefaultValue));
                }
            }
            catch
            {
                // DB may not exist yet
            }
        }

        private void ImportSwagger()
        {
            var openFileDialog = new OpenFileDialog
            {
                Filter = "JSON files (*.json)|*.json|Text files (*.txt)|*.txt|All files (*.*)|*.*",
                Title = "Import Swagger JSON File"
            };

            if (openFileDialog.ShowDialog() != true)
                return;

            try
            {
                var nodes = ParseSwaggerFile(openFileDialog.FileName);
                var fileName = Path.GetFileName(openFileDialog.FileName);
                SaveSwaggerNodesToDb(nodes, fileName);

                foreach (var node in nodes)
                {
                    SwaggerOperations.Add(node);
                    AvailableOperations.Add(node);
                    MenuAvailableOperations.Add(node);
                }

                RebuildGroupedSwaggerOperations();

                MessageBox.Show($"Successfully imported {nodes.Count} API endpoints from Swagger.", "Import Swagger", MessageBoxButton.OK, MessageBoxImage.Information);
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Failed to parse Swagger file: {ex.Message}", "Import Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }

        private List<OperationInfoViewModel> ParseSwaggerFile(string jsonFilePath)
        {
            var operations = new List<OperationInfoViewModel>();
            var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result;
            var createdModels = new HashSet<string>();

            foreach (var path in openApiDocument.Paths)
            {
                foreach (var method in path.Value)
                {
                    // Determine swagger group from operation tags or path segments
                    var tag = (method.Value.Tags != null && method.Value.Tags.Count > 0)
                        ? method.Value.Tags[0]
                        : null;
                    var group = !string.IsNullOrEmpty(tag)
                        ? tag
                        : ExtractGroupFromPath(path.Key);

                    var ovmodel = new OperationInfoViewModel
                    {
                        Title = path.Key,
                        OPType = method.Key,
                        Type = OperationType.API,
                        SwaggerGroup = group
                    };

                    var addedParams = new HashSet<string>();
                    foreach (var parameter in method.Value.Parameters)
                    {
                        if (addedParams.Add(parameter.Name))
                        {
                            ovmodel.Input.Add(parameter.Name);
                        }
                    }

                    // Extract response model from 200/201 response schema
                    var responseModelName = ExtractResponseModel(method.Value, path.Key, method.Key, createdModels);
                    ovmodel.ResponseModelClassName = responseModelName;

                    ovmodel.Output.Add("");
                    operations.Add(ovmodel);
                }
            }

            return operations;
        }

        private string ExtractResponseModel(OpenApiOperation operation, string path, string httpMethod, HashSet<string> createdModels)
        {
            try
            {
                // Check 200 and 201 responses for schema
                foreach (var statusCode in new[] { "200", "201", "default" })
                {
                    if (!operation.Responses.ContainsKey(statusCode))
                        continue;

                    var response = operation.Responses[statusCode];

                    // Try OpenAPI 2.0 style (response.Schema) first, then OpenAPI 3.0 (response.Content)
                    NJsonSchema.JsonSchema schema = response.Schema;
                    if (schema == null && response.Content != null && response.Content.Count > 0)
                    {
                        // OpenAPI 3.0: look for application/json or take first content entry
                        if (response.Content.TryGetValue("application/json", out var jsonContent))
                            schema = jsonContent.Schema;
                        else
                            schema = response.Content.Values.FirstOrDefault()?.Schema;
                    }
                    if (schema == null)
                        continue;

                    // Resolve $ref references
                    var actualSchema = schema.ActualSchema ?? schema;

                    // Resolve the actual item schema for arrays
                    var targetSchema = actualSchema;
                    bool isList = false;
                    if (actualSchema.Type == NJsonSchema.JsonObjectType.Array && actualSchema.Item != null)
                    {
                        targetSchema = actualSchema.Item.ActualSchema ?? actualSchema.Item;
                        isList = true;
                    }

                    // Resolve again in case of nested $ref
                    targetSchema = targetSchema.ActualSchema ?? targetSchema;

                    // Get or derive class name from schema title, definition name, or path
                    var className = !string.IsNullOrEmpty(targetSchema.Title)
                        ? targetSchema.Title
                        : !string.IsNullOrEmpty(targetSchema.DocumentPath) ? System.IO.Path.GetFileNameWithoutExtension(targetSchema.DocumentPath)
                        : DeriveClassName(path, httpMethod);

                    // Also try to get name from the schema's Id (definition key)
                    if (string.IsNullOrEmpty(className) || className == "swagger" || className == "openapi")
                        className = DeriveClassName(path, httpMethod);

                    className = SanitizeClassName(className);

                    // Use ActualProperties which resolves inherited/allOf properties
                    var props = targetSchema.ActualProperties;
                    if (string.IsNullOrEmpty(className) || props == null || props.Count == 0)
                        continue;

                    // Create model .cs file if not already created
                    if (createdModels.Add(className))
                    {
                        GenerateModelFromSchema(className, targetSchema);
                        // Add to AvailableModels
                        var modelInfo = new OperationInfoViewModel
                        {
                            Title = className,
                            IsModelNode = true,
                            Type = OperationType.System,
                            sysOp = SystemOperations.GET_SET,
                            ClassName = className
                        };
                        AddNewModel(modelInfo);
                    }

                    return isList ? $"List<{className}>" : className;
                }
            }
            catch { /* Response schema parsing is best-effort */ }

            return string.Empty;
        }

        private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema)
        {
            var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
            Directory.CreateDirectory(customModelDir);
            var sb = new System.Text.StringBuilder();
            sb.AppendLine($"public class {className}");
            sb.AppendLine("{");
            // Use ActualProperties to resolve $ref and allOf/inheritance
            var props = schema.ActualProperties != null && schema.ActualProperties.Count > 0
                ? (IEnumerable<KeyValuePair<string, NJsonSchema.JsonSchemaProperty>>)schema.ActualProperties
                : schema.Properties;
            foreach (var prop in props)
            {
                var propType = MapJsonSchemaType(prop.Value);
                var propName = Executor.ToPascalCase(prop.Key);
                sb.AppendLine($"\tpublic {propType} {propName} {{ get; set; }}");
            }
            sb.AppendLine("}");
            File.WriteAllText(Path.Combine(customModelDir, $"{className}.cs"), sb.ToString());
        }

        private static string MapJsonSchemaType(NJsonSchema.JsonSchemaProperty prop)
        {
            // Resolve the actual schema in case of $ref
            var actual = prop.ActualSchema ?? prop;
            var type = actual.Type;
            if (type.HasFlag(NJsonSchema.JsonObjectType.String)) return "string";
            if (type.HasFlag(NJsonSchema.JsonObjectType.Integer)) return "int";
            if (type.HasFlag(NJsonSchema.JsonObjectType.Number)) return "double";
            if (type.HasFlag(NJsonSchema.JsonObjectType.Boolean)) return "bool";
            if (type.HasFlag(NJsonSchema.JsonObjectType.Array)) return "List<object>";
            if (type.HasFlag(NJsonSchema.JsonObjectType.Object)) return "object";
            // If type is None but has properties, it's likely an object via $ref
            if (type == NJsonSchema.JsonObjectType.None && actual.Properties.Count > 0) return "object";
            return "string";
        }

        private static string DeriveClassName(string path, string httpMethod)
        {
            // e.g. /api/users/{id} + get -> GetUsersResponse
            var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries)
                .Where(s => !s.StartsWith("{"))
                .Select(s => Executor.ToPascalCase(s));
            return $"{Executor.ToPascalCase(httpMethod)}{string.Join("", segments)}Response";
        }

        private static string SanitizeClassName(string name)
        {
            return new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
        }

        private static string ExtractGroupFromPath(string path)
        {
            // Extract group from path like "/api/Pizza/{id}" -> "Pizza"
            var segments = path.Split('/', System.StringSplitOptions.RemoveEmptyEntries);
            // Skip "api" prefix if present, take the first meaningful segment
            foreach (var seg in segments)
            {
                if (seg.StartsWith("{") || string.Equals(seg, "api", System.StringComparison.OrdinalIgnoreCase))
                    continue;
                // Capitalize first letter
                return char.ToUpper(seg[0]) + seg.Substring(1);
            }
            return "Other";
        }

        private void RebuildGroupedSwaggerOperations()
        {
            GroupedSwaggerOperations.Clear();
            var groups = SwaggerOperations
                .GroupBy(op => string.IsNullOrEmpty(op.SwaggerGroup) ? "Other" : op.SwaggerGroup)
                .OrderBy(g => g.Key);

            foreach (var group in groups)
            {
                GroupedSwaggerOperations.Add(new SwaggerGroupViewModel
                {
                    GroupName = group.Key,
                    Operations = new NodifyObservableCollection<OperationInfoViewModel>(group.ToList())
                });
            }
        }

        private void SaveSwaggerNodesToDb(List<OperationInfoViewModel> nodes, string swaggerFileName)
        {
            using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
            db.DeleteMany(n => n.SwaggerFileName == swaggerFileName);

            foreach (var node in nodes)
            {
                db.Insert(new SwaggerNodeModel
                {
                    Title = node.Title ?? string.Empty,
                    OPType = node.OPType ?? string.Empty,
                    InputNames = new List<string>(node.Input),
                    SwaggerFileName = swaggerFileName,
                    ResponseModelClassName = node.ResponseModelClassName ?? string.Empty,
                    SwaggerGroup = node.SwaggerGroup ?? string.Empty
                });
            }
        }

        private void LoadSwaggerNodesFromDb()
        {
            try
            {
                using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
                var savedNodes = db.FindAll();

                foreach (var saved in savedNodes)
                {
                    var savedGroup = saved.SwaggerGroup;
                    if (string.IsNullOrEmpty(savedGroup))
                        savedGroup = ExtractGroupFromPath(saved.Title ?? "");

                    var ovmodel = new OperationInfoViewModel
                    {
                        Title = saved.Title,
                        OPType = saved.OPType,
                        Type = OperationType.API,
                        SwaggerGroup = savedGroup
                    };

                    foreach (var inputName in saved.InputNames)
                    {
                        ovmodel.Input.Add(inputName);
                    }

                    ovmodel.ResponseModelClassName = saved.ResponseModelClassName ?? string.Empty;
                    ovmodel.Output.Add("");
                    SwaggerOperations.Add(ovmodel);
                }
            }
            catch
            {
                // DB may not exist yet on first run
            }
        }

        private void CreateOperation(OperationInfoViewModel operationInfo)
        {
            OperationViewModel op = OperationFactory.GetOperation(operationInfo);
            op.Location = Location;

            _calculator.Operations.Add(op);

            var pending = _calculator.PendingConnection;
            if (pending.IsVisible)
            {
                var connector = pending.Source.IsInput ? op.Output.FirstOrDefault() : op.Input.FirstOrDefault();
                if (connector != null && _calculator.CanCreateConnection(pending.Source, connector))
                {
                    _calculator.CreateConnection(pending.Source, connector);
                }
            }
            Close();
        }
    }
}
