using System.Text.Json;
using BlazorMonaco;
using BlazorMonaco.Languages;

namespace Inspectron.PrintServer.TemplateEditor.Services;

public class TemplateCompletionProvider
{
    private List<JsonPropertyInfo> _jsonPaths = [];

    public void SetJsonPaths(List<JsonPropertyInfo> paths) => _jsonPaths = paths;

    public Task<CompletionList> ProvideCompletionItems(string modelUri, Position position, CompletionContext context)
    {
        var items = new List<CompletionItem>();
        var editorContent = _editorContentAccessor?.Invoke();

        if (string.IsNullOrEmpty(editorContent))
            return Task.FromResult(new CompletionList { Suggestions = items });

        var lines = editorContent.Split('\n');
        if (position.LineNumber < 1 || position.LineNumber > lines.Length)
            return Task.FromResult(new CompletionList { Suggestions = items });

        var line = lines[position.LineNumber - 1];
        var col = Math.Min(position.Column - 1, line.Length);
        var textBefore = line[..col];

        var completionContext = DetectContext(textBefore, lines, position.LineNumber);

        switch (completionContext.Kind)
        {
            case ContextKind.ElementName:
                items = GetElementCompletions(completionContext.Prefix);
                break;
            case ContextKind.AttributeName:
                items = GetAttributeCompletions(completionContext.ElementName, completionContext.Prefix);
                break;
            case ContextKind.AttributeValue:
                items = GetAttributeValueCompletions(completionContext.ElementName, completionContext.AttributeName);
                break;
            case ContextKind.Expression:
                items = GetExpressionCompletions(completionContext.Prefix, lines, position.LineNumber);
                break;
        }

        return Task.FromResult(new CompletionList { Suggestions = items });
    }

    public Task<CompletionItem> ResolveCompletionItem(CompletionItem item) => Task.FromResult(item);

    private Func<string>? _editorContentAccessor;

    public void SetEditorContentAccessor(Func<string> accessor) => _editorContentAccessor = accessor;

    #region Context Detection

    private enum ContextKind { None, ElementName, AttributeName, AttributeValue, Expression }

    private record CompletionContextInfo(ContextKind Kind, string Prefix = "", string ElementName = "", string AttributeName = "");

    private static CompletionContextInfo DetectContext(string textBefore, string[] lines, int lineNumber)
    {
        // Inside {{ }} expression?
        var lastOpen = textBefore.LastIndexOf("{{", StringComparison.Ordinal);
        var lastClose = textBefore.LastIndexOf("}}", StringComparison.Ordinal);
        if (lastOpen >= 0 && lastOpen > lastClose)
        {
            var exprText = textBefore[(lastOpen + 2)..];
            return new CompletionContextInfo(ContextKind.Expression, exprText);
        }

        // Inside attribute value (="...")?
        var inAttrValue = IsInsideAttributeValue(textBefore);
        if (inAttrValue.Inside)
            return new CompletionContextInfo(ContextKind.AttributeValue,
                ElementName: inAttrValue.ElementName, AttributeName: inAttrValue.AttributeName);

        // After < for element name?
        var lastLt = textBefore.LastIndexOf('<');
        var lastGt = textBefore.LastIndexOf('>');
        if (lastLt >= 0 && lastLt > lastGt)
        {
            var afterLt = textBefore[(lastLt + 1)..];

            // Closing tag </...
            if (afterLt.StartsWith("/"))
            {
                var prefix = afterLt[1..];
                return new CompletionContextInfo(ContextKind.ElementName, prefix);
            }

            // Check if we're still on the element name or moved to attributes
            var spaceIdx = afterLt.IndexOf(' ');
            if (spaceIdx < 0)
            {
                // Still typing element name
                return new CompletionContextInfo(ContextKind.ElementName, afterLt);
            }

            // After element name + space = attribute context
            var elemName = afterLt[..spaceIdx].Trim();
            var attrText = afterLt[spaceIdx..].TrimStart();
            // Get the last partial attribute name being typed
            var lastSpace = attrText.LastIndexOfAny([' ', '"', '\'']);
            var attrPrefix = lastSpace >= 0 ? attrText[(lastSpace + 1)..] : attrText;
            // Filter out if we're right after = or inside a value
            if (!attrPrefix.Contains('='))
                return new CompletionContextInfo(ContextKind.AttributeName, attrPrefix, elemName);
        }

        return new CompletionContextInfo(ContextKind.None);
    }

    private static (bool Inside, string ElementName, string AttributeName) IsInsideAttributeValue(string text)
    {
        // Find the last opening tag context
        var lastLt = text.LastIndexOf('<');
        if (lastLt < 0) return (false, "", "");

        var tagContent = text[(lastLt + 1)..];
        var lastGt = tagContent.LastIndexOf('>');
        if (lastGt >= 0) return (false, "", "");

        // Extract element name
        var spaceIdx = tagContent.IndexOf(' ');
        if (spaceIdx < 0) return (false, "", "");
        var elemName = tagContent[..spaceIdx];
        if (elemName.StartsWith("/")) return (false, "", "");

        // Check if cursor is inside a quoted attribute value
        var attrPart = tagContent[spaceIdx..];
        var inDoubleQuote = false;
        var inSingleQuote = false;
        var lastAttrName = "";
        var i = 0;

        while (i < attrPart.Length)
        {
            if (attrPart[i] == '"' && !inSingleQuote)
            {
                inDoubleQuote = !inDoubleQuote;
            }
            else if (attrPart[i] == '\'' && !inDoubleQuote)
            {
                inSingleQuote = !inSingleQuote;
            }
            else if (!inDoubleQuote && !inSingleQuote && char.IsLetter(attrPart[i]))
            {
                // Capture attribute name
                var start = i;
                while (i < attrPart.Length && (char.IsLetterOrDigit(attrPart[i]) || attrPart[i] == '-' || attrPart[i] == '_'))
                    i++;
                lastAttrName = attrPart[start..i];
                continue;
            }

            i++;
        }

        if (inDoubleQuote || inSingleQuote)
            return (true, elemName, lastAttrName);

        return (false, "", "");
    }

    #endregion

    #region Element Schema

    private static readonly Dictionary<string, ElementSchema> Schema = new(StringComparer.OrdinalIgnoreCase)
    {
        ["receipt"] = new("Required root element", []),
        ["line"] = new("Single line of text", new()
        {
            ["align"] = new(["left", "center", "right"], "Text alignment"),
            ["bold"] = BoolAttr("Bold text"),
            ["big"] = BoolAttr("Double-width font"),
            ["tall"] = BoolAttr("Double-height font"),
            ["red"] = BoolAttr("Red text (supported printers)"),
            ["lineSpacing"] = new([], "Line spacing in dots"),
            ["wrap"] = BoolAttr("Word-wrap text"),
            ["wrapIndent"] = new([], "Indent for wrapped lines"),
        }),
        ["columns"] = new("Two-column layout", new()
        {
            ["left"] = new([], "Left column content"),
            ["right"] = new([], "Right column content"),
            ["bold"] = BoolAttr("Bold text"),
            ["big"] = BoolAttr("Double-width font"),
            ["tall"] = BoolAttr("Double-height font"),
            ["red"] = BoolAttr("Red text"),
            ["lineSpacing"] = new([], "Line spacing in dots"),
            ["wrap"] = BoolAttr("Wrap left column"),
            ["wrapIndent"] = new([], "Indent for wrapped lines"),
        }),
        ["row"] = new("Multi-column layout with <col> children", new()
        {
            ["bold"] = BoolAttr("Bold text"),
            ["big"] = BoolAttr("Double-width font"),
            ["tall"] = BoolAttr("Double-height font"),
            ["red"] = BoolAttr("Red text"),
            ["lineSpacing"] = new([], "Line spacing in dots"),
        }),
        ["col"] = new("Column inside <row> or <table>", new()
        {
            ["width"] = new([], "Column width (char count or percentage)"),
            ["align"] = new(["left", "center", "right"], "Cell alignment"),
            ["wrap"] = BoolAttr("Word-wrap cell text"),
        }),
        ["separator"] = new("Repeated character line", new()
        {
            ["char"] = new([], "Character to repeat (default: -)"),
        }),
        ["cut"] = new("Paper cut command", []),
        ["feed"] = new("Empty lines", new()
        {
            ["lines"] = new([], "Number of empty lines"),
        }),
        ["table"] = new("ASCII box table", new()
        {
            ["items"] = new([], "JSON array path for data rows"),
            ["var"] = new([], "Loop variable name"),
            ["headerItems"] = new([], "JSON array path for header rows"),
        }),
        ["foreach"] = new("Loop over array", new()
        {
            ["items"] = new([], "Path to JSON array (required)"),
            ["var"] = new([], "Loop variable name (required)"),
        }),
        ["if"] = new("Conditional block", new()
        {
            ["test"] = new([], "Condition expression (required)"),
        }),
        ["else"] = new("Else branch (must follow <if>)", []),
    };

    private static AttributeSchema BoolAttr(string desc) => new(["true", "false"], desc);

    private record ElementSchema(string Description, Dictionary<string, AttributeSchema> Attributes);
    private record AttributeSchema(List<string> Values, string Description);

    #endregion

    #region Completion Generators

    private static List<CompletionItem> GetElementCompletions(string prefix)
    {
        var items = new List<CompletionItem>();
        var sortIndex = 0;

        foreach (var (name, schema) in Schema)
        {
            items.Add(new CompletionItem
            {
                LabelAsString = name,
                Kind = CompletionItemKind.Keyword,
                Detail = schema.Description,
                InsertText = GetElementSnippet(name, schema),
                InsertTextRules = CompletionItemInsertTextRule.InsertAsSnippet,
                SortText = sortIndex++.ToString("D3"),
            });
        }

        return items;
    }

    private static string GetElementSnippet(string name, ElementSchema schema)
    {
        return name switch
        {
            "line" => "line${1: align=\"${2:left}\"}>${3:$0}</line>",
            "columns" => "columns left=\"${1}\" right=\"${2}\" />",
            "row" => "row>\n\t<col align=\"${1:left}\">${2}</col>\n\t<col align=\"${3:right}\">${4}</col>\n</row>",
            "col" => "col${1: align=\"${2:left}\"}>${3:$0}</col>",
            "separator" => "separator />",
            "cut" => "cut />",
            "feed" => "feed${1: lines=\"${2:1}\"} />",
            "foreach" => "foreach items=\"${1}\" var=\"${2:item}\">\n\t$0\n</foreach>",
            "if" => "if test=\"${1}\">\n\t$0\n</if>",
            "else" => "else>\n\t$0\n</else>",
            "table" => "table items=\"${1}\" var=\"${2:item}\">\n\t<col width=\"${3}\" align=\"${4:left}\">${5:Header}</col>\n</table>",
            "receipt" => "receipt>\n\t$0\n</receipt>",
            _ => $"{name}>$0</{name}>"
        };
    }

    private static List<CompletionItem> GetAttributeCompletions(string elementName, string prefix)
    {
        var items = new List<CompletionItem>();

        if (!Schema.TryGetValue(elementName, out var schema))
            return items;

        var sortIndex = 0;
        foreach (var (attrName, attrSchema) in schema.Attributes)
        {
            var insertText = attrSchema.Values.Count > 0
                ? $"{attrName}=\"${{1|{string.Join(",", attrSchema.Values)}|}}\""
                : $"{attrName}=\"$1\"";

            items.Add(new CompletionItem
            {
                LabelAsString = attrName,
                Kind = CompletionItemKind.Property,
                Detail = attrSchema.Description,
                InsertText = insertText,
                InsertTextRules = CompletionItemInsertTextRule.InsertAsSnippet,
                SortText = sortIndex++.ToString("D3"),
            });
        }

        return items;
    }

    private static List<CompletionItem> GetAttributeValueCompletions(string elementName, string attributeName)
    {
        var items = new List<CompletionItem>();

        if (!Schema.TryGetValue(elementName, out var schema))
            return items;

        if (!schema.Attributes.TryGetValue(attributeName, out var attrSchema))
            return items;

        foreach (var value in attrSchema.Values)
        {
            items.Add(new CompletionItem
            {
                LabelAsString = value,
                Kind = CompletionItemKind.Value,
                InsertText = value,
            });
        }

        return items;
    }

    private List<CompletionItem> GetExpressionCompletions(string prefix, string[] lines, int lineNumber)
    {
        var items = new List<CompletionItem>();
        var sortIndex = 0;

        // Loop variables
        var loopVars = FindLoopVariables(lines, lineNumber);

        // Special loop variables
        foreach (var special in new[] { "$index", "$first", "$last" })
        {
            if (loopVars.Count > 0) // Only suggest if inside a foreach
            {
                items.Add(new CompletionItem
                {
                    LabelAsString = special,
                    Kind = CompletionItemKind.Variable,
                    Detail = special switch
                    {
                        "$index" => "Zero-based loop index",
                        "$first" => "True on first iteration",
                        "$last" => "True on last iteration",
                        _ => ""
                    },
                    InsertText = special,
                    SortText = sortIndex++.ToString("D3"),
                });
            }
        }

        // Loop variable property paths from JSON
        foreach (var loopVar in loopVars)
        {
            // Find array in JSON paths and suggest its item properties
            var arrayPaths = FindMatchingArrayPaths(loopVar.ItemsPath);

            foreach (var prop in arrayPaths)
            {
                // prop.Path is like "Items[].Description" - we want "item.Description"
                var suffix = prop.Path;
                var bracketIdx = suffix.IndexOf("[].", StringComparison.Ordinal);
                if (bracketIdx >= 0)
                    suffix = suffix[(bracketIdx + 3)..];
                else
                    continue;

                var label = $"{loopVar.VarName}.{suffix}";

                items.Add(new CompletionItem
                {
                    LabelAsString = label,
                    Kind = CompletionItemKind.Field,
                    Detail = $"{prop.Type} (from {loopVar.ItemsPath})",
                    InsertText = label,
                    SortText = sortIndex++.ToString("D3"),
                });
            }
        }

        // Top-level JSON paths
        foreach (var prop in _jsonPaths.Where(p => !p.Path.Contains("[].")))
        {
            var kind = prop.IsArray ? CompletionItemKind.Enum : CompletionItemKind.Field;
            items.Add(new CompletionItem
            {
                LabelAsString = prop.Path,
                Kind = kind,
                Detail = prop.Type,
                InsertText = prop.Path,
                SortText = sortIndex++.ToString("D3"),
            });
        }

        // Format specifiers
        if (prefix.Contains(':'))
        {
            items.Clear();
            foreach (var (fmt, desc) in FormatSpecifiers)
            {
                items.Add(new CompletionItem
                {
                    LabelAsString = fmt,
                    Kind = CompletionItemKind.Unit,
                    Detail = desc,
                    InsertText = fmt,
                });
            }
        }

        return items;
    }

    private static readonly (string Format, string Description)[] FormatSpecifiers =
    [
        ("F2", "Fixed-point 2 decimals (9.50)"),
        ("F0", "Fixed-point no decimals (10)"),
        ("N0", "Number with separators (1,234)"),
        ("N2", "Number with separators + decimals (1,234.56)"),
        ("dd.MM.yyyy", "Date (15.03.2024)"),
        ("HH:mm", "Time (14:30)"),
        ("HH:mm:ss", "Time with seconds (14:30:00)"),
        ("dd-MMM-yy HH:mm", "Date + time (15-Mar-24 14:30)"),
        ("dd.MM.yyyy HH:mm", "Date + time (15.03.2024 14:30)"),
    ];

    #endregion

    #region Loop Variable Detection

    private record LoopVarInfo(string VarName, string ItemsPath);

    private static List<LoopVarInfo> FindLoopVariables(string[] lines, int lineNumber)
    {
        var vars = new List<LoopVarInfo>();

        // Walk backwards from current line to find enclosing foreach elements
        var depth = 0;
        for (var i = lineNumber - 1; i >= 0; i--)
        {
            var line = lines[i].Trim();

            // Count closing foreach tags (increase depth needed)
            if (line.Contains("</foreach>"))
                depth++;

            // Check for opening foreach tags
            var foreachMatch = System.Text.RegularExpressions.Regex.Match(line,
                @"<foreach\s+items=""([^""]+)""\s+var=""([^""]+)""");
            if (!foreachMatch.Success)
                foreachMatch = System.Text.RegularExpressions.Regex.Match(line,
                    @"<foreach\s+var=""([^""]+)""\s+items=""([^""]+)""");

            if (foreachMatch.Success)
            {
                if (depth > 0)
                {
                    depth--;
                    continue;
                }

                string itemsPath, varName;
                if (foreachMatch.Groups.Count >= 3)
                {
                    // Determine which capture is items vs var based on pattern
                    if (line.IndexOf("items=", StringComparison.Ordinal) < line.IndexOf("var=", StringComparison.Ordinal))
                    {
                        itemsPath = foreachMatch.Groups[1].Value;
                        varName = foreachMatch.Groups[2].Value;
                    }
                    else
                    {
                        varName = foreachMatch.Groups[1].Value;
                        itemsPath = foreachMatch.Groups[2].Value;
                    }

                    vars.Add(new LoopVarInfo(varName, itemsPath));
                }
            }
        }

        return vars;
    }

    private List<JsonPropertyInfo> FindMatchingArrayPaths(string itemsPath)
    {
        // itemsPath could be "Items" or "gang.Dishes" etc.
        // We need to find JSON paths that start with the matching array path + "[]."
        return _jsonPaths
            .Where(p => p.Path.StartsWith(itemsPath + "[].", StringComparison.OrdinalIgnoreCase))
            .ToList();
    }

    #endregion
}
