using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Sbroenne.ExcelMcp.Generators.Common;

/// <summary>
/// Extracts ServiceInfo from interfaces marked with [ServiceCategory].
/// Shared between all generators.
/// </summary>
public static class ServiceInfoExtractor
{
    public static ServiceInfo? ExtractServiceInfo(INamedTypeSymbol interfaceSymbol)
    {
        string? category = null;
        string? pascalName = null;
        string? mcpTool = null;
        bool noSession = false;
        string? mcpToolTitle = null;
        bool mcpToolDestructive = true;
        string? mcpToolCategory = null;
        string? mcpToolDescription = null;

        foreach (var attr in interfaceSymbol.GetAttributes())
        {
            var attrName = attr.AttributeClass?.Name;

            if (attrName == "ServiceCategoryAttribute")
            {
                if (attr.ConstructorArguments.Length > 0)
                {
                    category = attr.ConstructorArguments[0].Value?.ToString();
                }
                if (attr.ConstructorArguments.Length > 1)
                {
                    pascalName = attr.ConstructorArguments[1].Value?.ToString();
                }
            }
            else if (attrName == "McpToolAttribute")
            {
                if (attr.ConstructorArguments.Length > 0)
                {
                    mcpTool = attr.ConstructorArguments[0].Value?.ToString();
                }

                // Read named properties: Title, Destructive, Category
                foreach (var namedArg in attr.NamedArguments)
                {
                    switch (namedArg.Key)
                    {
                        case "Title":
                            mcpToolTitle = namedArg.Value.Value?.ToString();
                            break;
                        case "Destructive":
                            if (namedArg.Value.Value is bool destructive)
                                mcpToolDestructive = destructive;
                            break;
                        case "Category":
                            mcpToolCategory = namedArg.Value.Value?.ToString();
                            break;
                        case "Description":
                            mcpToolDescription = namedArg.Value.Value?.ToString();
                            break;
                    }
                }
            }
            else if (attrName == "NoSessionAttribute")
            {
                noSession = true;
            }
        }

        if (category is null)
            return null;

        // Extract interface-level XML documentation
        var interfaceSummary = ExtractInterfaceSummary(interfaceSymbol);

        var methods = new List<MethodInfo>();

        foreach (var member in interfaceSymbol.GetMembers())
        {
            if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary)
            {
                var actionName = GetActionName(method);
                var methodMcpTool = GetMethodMcpTool(method) ?? mcpTool;
                var xmlDoc = ExtractXmlDocumentation(method);

                var hasBatchParameter = method.Parameters.Any(p => p.Type.Name == "IExcelBatch");
                var hasProgressParameter = method.Parameters.Any(p => p.Type.Name == "IProgress");
                var parameters = method.Parameters
                    .Where(p => p.Type.Name != "IExcelBatch" && p.Type.Name != "IProgress") // Skip batch and progress parameters
                    .Select(p => ExtractParameterInfo(p, xmlDoc))
                    .ToList();

                methods.Add(new MethodInfo(
                    method.Name,
                    actionName,
                    TypeNameHelper.GetTypeName(method.ReturnType),
                    methodMcpTool ?? category,
                    parameters,
                    xmlDoc?.Summary,
                    hasBatchParameter,
                    hasProgressParameter));
            }
        }

        // Use explicit pascalName if provided, otherwise derive from category
        var categoryPascal = pascalName ?? StringHelper.ToPascalCase(category);

        return new ServiceInfo(
            category,
            categoryPascal,
            mcpTool ?? category,
            noSession,
            methods,
            interfaceSummary,
            mcpToolTitle,
            mcpToolDestructive,
            mcpToolCategory,
            mcpToolDescription,
            hasMcpToolAttribute: mcpTool != null);
    }

    private static string? ExtractInterfaceSummary(INamedTypeSymbol interfaceSymbol)
    {
        var xmlComment = interfaceSymbol.GetDocumentationCommentXml();
        if (string.IsNullOrEmpty(xmlComment))
            return null;

        try
        {
            var doc = new XmlDocument();
            doc.LoadXml($"<root>{xmlComment}</root>");
            var text = doc.SelectSingleNode("//summary")?.InnerText?.Trim();
            if (string.IsNullOrEmpty(text))
                return null;

            // Normalize multi-line XML doc comments: collapse whitespace runs into single space
            return Regex.Replace(text, @"\s+", " ");
        }
        catch (Exception)
        {
            // XML comment parsing is best-effort — malformed XML should not break generation
            return null;
        }
    }

    private static string GetActionName(IMethodSymbol method)
    {
        // Check for [ServiceAction] override
        foreach (var attr in method.GetAttributes())
        {
            if (attr.AttributeClass?.Name == "ServiceActionAttribute" && attr.ConstructorArguments.Length > 0)
            {
                return attr.ConstructorArguments[0].Value?.ToString() ?? StringHelper.ToKebabCase(method.Name);
            }
        }

        // Default: derive from method name
        return StringHelper.ToKebabCase(method.Name);
    }

    private static string? GetMethodMcpTool(IMethodSymbol method)
    {
        foreach (var attr in method.GetAttributes())
        {
            if (attr.AttributeClass?.Name == "McpToolAttribute" && attr.ConstructorArguments.Length > 0)
            {
                return attr.ConstructorArguments[0].Value?.ToString();
            }
        }
        return null;
    }

    private static ParameterInfo ExtractParameterInfo(IParameterSymbol param, XmlDocumentation? methodDoc)
    {
        bool isFileOrValue = false;
        string? fileSuffix = null;
        bool isFromString = false;
        string? exposedName = null;
        bool isRequired = false;

        foreach (var attr in param.GetAttributes())
        {
            var attrName = attr.AttributeClass?.Name;

            if (attrName == "FileOrValueAttribute")
            {
                isFileOrValue = true;
                if (attr.ConstructorArguments.Length > 0)
                {
                    fileSuffix = attr.ConstructorArguments[0].Value?.ToString() ?? "File";
                }
                else
                {
                    fileSuffix = "File";
                }
            }
            else if (attrName == "FromStringAttribute")
            {
                isFromString = true;
                if (attr.ConstructorArguments.Length > 0)
                {
                    exposedName = attr.ConstructorArguments[0].Value?.ToString();
                }
            }
            else if (attrName == "RequiredParameterAttribute")
            {
                isRequired = true;
            }
        }

        // Detect if this is an enum type (including Nullable<Enum>)
        bool isEnum = param.Type.TypeKind == TypeKind.Enum;
        string? enumTypeName = null;
        if (isEnum)
        {
            enumTypeName = TypeNameHelper.GetTypeName(param.Type);
        }
        else if (param.Type is INamedTypeSymbol nullableType
            && nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T
            && nullableType.TypeArguments.Length == 1)
        {
            isEnum = nullableType.TypeArguments[0].TypeKind == TypeKind.Enum;
            if (isEnum)
            {
                enumTypeName = TypeNameHelper.GetTypeName(nullableType.TypeArguments[0]);
            }
        }

        // Get XML doc description for this parameter
        string? paramDescription = null;
        if (methodDoc?.Parameters != null && methodDoc.Parameters.TryGetValue(param.Name, out var desc))
        {
            paramDescription = desc;
        }

        return new ParameterInfo(
            param.Name,
            TypeNameHelper.GetTypeName(param.Type, param.NullableAnnotation),
            param.HasExplicitDefaultValue,
            param.HasExplicitDefaultValue ? TypeNameHelper.GetDefaultValueString(param) : null,
            isFileOrValue,
            fileSuffix,
            isFromString,
            exposedName,
            isRequired,
            isEnum,
            paramDescription,
            enumTypeName);
    }

    private static XmlDocumentation? ExtractXmlDocumentation(IMethodSymbol method)
    {
        var xmlComment = method.GetDocumentationCommentXml();
        if (string.IsNullOrEmpty(xmlComment))
            return null;

        try
        {
            var doc = new XmlDocument();
            doc.LoadXml($"<root>{xmlComment}</root>");

            var summary = doc.SelectSingleNode("//summary")?.InnerText?.Trim();
            var parameters = new Dictionary<string, string>();

            var paramNodes = doc.SelectNodes("//param");
            if (paramNodes != null)
            {
                foreach (XmlNode paramNode in paramNodes)
                {
                    var name = paramNode.Attributes?["name"]?.Value;
                    var description = paramNode.InnerText?.Trim();
                    if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(description))
                    {
                        parameters[name] = description;
                    }
                }
            }

            return new XmlDocumentation(summary, parameters);
        }
        catch (Exception)
        {
            // XML doc parsing is best-effort — malformed XML should not break generation
            return null;
        }
    }

    /// <summary>
    /// Gets all unique exposed parameters across all methods in a service.
    /// Tracks which actions require each parameter for description enrichment.
    /// </summary>
    public static List<ExposedParameter> GetAllExposedParameters(ServiceInfo info)
    {
        var paramMap = new Dictionary<string, ExposedParameter>(StringComparer.OrdinalIgnoreCase);

        foreach (var method in info.Methods)
        {
            foreach (var p in method.Parameters)
            {
                // Get the exposed name (from attribute or original name)
                var exposedName = p.ExposedName ?? p.Name;

                if (!paramMap.TryGetValue(exposedName, out var existing))
                {
                    existing = new ExposedParameter(exposedName, p.TypeName, p.XmlDocDescription);
                    paramMap[exposedName] = existing;
                }
                else if (p.TypeName.EndsWith("?") && !existing.TypeName.EndsWith("?"))
                {
                    // If any method declares this parameter as nullable, upgrade to nullable.
                    // MCP parameters are shared across all actions and must be compatible with ALL uses.
                    existing.TypeName = p.TypeName;
                }

                // Track if this param is required for this action
                var isRequired = p.IsRequired || (!p.HasDefault && !p.TypeName.EndsWith("?"));
                if (isRequired)
                {
                    existing.RequiredByActions.Add(method.ActionName);
                }

                // If FileOrValue, also add the file variant
                if (p.IsFileOrValue && p.FileSuffix != null)
                {
                    var fileParamName = exposedName + p.FileSuffix;
                    if (!paramMap.ContainsKey(fileParamName))
                    {
                        paramMap[fileParamName] = new ExposedParameter(fileParamName, "string?", $"Path to file containing {exposedName}");
                    }
                }
            }
        }

        // Set total action count on all params
        var totalActions = info.Methods.Count;
        foreach (var ep in paramMap.Values)
        {
            ep.TotalActionCount = totalActions;
        }

        return paramMap.Values.ToList();
    }
}

/// <summary>
/// Extracted XML documentation from a method.
/// </summary>
public sealed class XmlDocumentation
{
    public string? Summary { get; }
    public Dictionary<string, string> Parameters { get; }

    public XmlDocumentation(string? summary, Dictionary<string, string> parameters)
    {
        Summary = summary;
        Parameters = parameters;
    }
}
