using System.Globalization;
using System.Xml.Linq;

namespace Inspectron.Epson.TemplateEngine.Parsing;

public class TemplateParser
{
    public ReceiptNode Parse(string xml)
    {
        XDocument doc;
        try
        {
            doc = XDocument.Parse(xml);
        }
        catch (Exception ex)
        {
            throw new TemplateParsingException($"Invalid XML: {ex.Message}", ex);
        }

        var root = doc.Root;
        if (root == null || root.Name.LocalName != "receipt")
            throw new TemplateParsingException("Root element must be <receipt>");

        var receiptNode = new ReceiptNode();
        ParseChildren(root, receiptNode.Children);
        return receiptNode;
    }

    private void ParseChildren(XElement parent, List<TemplateNode> children)
    {
        foreach (var element in parent.Elements())
        {
            var node = ParseElement(element);
            if (node != null)
                children.Add(node);
        }
    }

    private TemplateNode? ParseElement(XElement element)
    {
        return element.Name.LocalName switch
        {
            "line" => ParseLine(element),
            "columns" => ParseColumns(element),
            "row" => ParseRow(element),
            "separator" => ParseSeparator(element),
            "cut" => new CutNode(),
            "feed" => ParseFeed(element),
            "foreach" => ParseForeach(element),
            "if" => ParseIf(element),
            "else" => ParseElse(element),
            "table" => ParseTable(element),
            _ => throw new TemplateParsingException($"Unknown element: <{element.Name.LocalName}>")
        };
    }

    private LineNode ParseLine(XElement element)
    {
        var node = new LineNode();
        ApplyFormatting(element, node);

        // Text content: either inner text (with {{}} expressions) or empty line
        var text = GetTextContent(element);
        node.Text = text;

        node.Align = GetAttr(element, "align", "left");
        node.Wrap = GetBoolAttr(element, "wrap");
        node.WrapIndent = GetIntAttr(element, "wrapIndent", 0);

        return node;
    }

    private ColumnsNode ParseColumns(XElement element)
    {
        var node = new ColumnsNode();
        ApplyFormatting(element, node);

        node.Left = GetAttr(element, "left", "");
        node.Right = GetAttr(element, "right", "");
        node.Wrap = GetBoolAttr(element, "wrap");
        node.WrapIndent = GetIntAttr(element, "wrapIndent", 0);

        return node;
    }

    private RowNode ParseRow(XElement element)
    {
        var node = new RowNode();
        ApplyFormatting(element, node);

        foreach (var colElement in element.Elements("col"))
        {
            var col = new ColumnDef
            {
                Text = GetTextContent(colElement),
                Align = GetAttr(colElement, "align", "left"),
                Wrap = GetBoolAttr(colElement, "wrap")
            };
            ParseColumnWidth(colElement, col);
            node.Columns.Add(col);
        }

        return node;
    }

    private SeparatorNode ParseSeparator(XElement element)
    {
        var charAttr = GetAttr(element, "char", "-");
        return new SeparatorNode
        {
            Character = string.IsNullOrEmpty(charAttr) ? '-' : charAttr[0]
        };
    }

    private FeedNode ParseFeed(XElement element)
    {
        return new FeedNode
        {
            Lines = GetIntAttr(element, "lines", 1)
        };
    }

    private ForeachNode ParseForeach(XElement element)
    {
        var items = GetRequiredAttr(element, "items", "foreach")!;
        var var_ = GetRequiredAttr(element, "var", "foreach")!;

        var node = new ForeachNode { Items = items, Var = var_ };
        ParseChildren(element, node.Children);
        return node;
    }

    private IfNode ParseIf(XElement element)
    {
        var test = GetRequiredAttr(element, "test", "if")!;

        var node = new IfNode { Test = test };
        ParseChildren(element, node.Children);
        return node;
    }

    private ElseNode ParseElse(XElement element)
    {
        var node = new ElseNode();
        ParseChildren(element, node.Children);
        return node;
    }

    private TableNode ParseTable(XElement element)
    {
        var node = new TableNode
        {
            Items = GetAttr(element, "items", null),
            Var = GetAttr(element, "var", null),
            HeaderItems = GetAttr(element, "headerItems", null)
        };

        foreach (var colElement in element.Elements("col"))
        {
            var col = new ColumnDef
            {
                Text = GetTextContent(colElement),
                Align = GetAttr(colElement, "align", "left"),
                Wrap = GetBoolAttr(colElement, "wrap")
            };
            ParseColumnWidth(colElement, col);
            node.Columns.Add(col);
        }

        // Parse non-col children (col elements are consumed above as column definitions)
        foreach (var child in element.Elements().Where(e => e.Name.LocalName != "col"))
        {
            var childNode = ParseElement(child);
            if (childNode != null)
                node.Children.Add(childNode);
        }

        return node;
    }

    private static void ApplyFormatting(XElement element, LineNode node)
    {
        node.Bold = GetBoolAttr(element, "bold");
        node.Big = GetBoolAttr(element, "big");
        node.Tall = GetBoolAttr(element, "tall");
        node.Red = GetBoolAttr(element, "red");
        node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
    }

    private static void ApplyFormatting(XElement element, ColumnsNode node)
    {
        node.Bold = GetBoolAttr(element, "bold");
        node.Big = GetBoolAttr(element, "big");
        node.Tall = GetBoolAttr(element, "tall");
        node.Red = GetBoolAttr(element, "red");
        node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
    }

    private static void ApplyFormatting(XElement element, RowNode node)
    {
        node.Bold = GetBoolAttr(element, "bold");
        node.Big = GetBoolAttr(element, "big");
        node.Tall = GetBoolAttr(element, "tall");
        node.Red = GetBoolAttr(element, "red");
        node.LineSpacing = GetNullableIntAttr(element, "lineSpacing");
    }

    private static string GetTextContent(XElement element)
    {
        // Get all inner text content (may include {{}} expressions)
        // Use element.Value to get concatenated text of all text nodes
        if (!element.HasElements)
            return element.Value;

        // If element has child elements, only get direct text nodes
        return string.Concat(element.Nodes().OfType<XText>().Select(t => t.Value));
    }

    private static string? GetRequiredAttr(XElement element, string name, string elementName)
    {
        var attr = element.Attribute(name);
        if (attr == null)
            throw new TemplateParsingException($"<{elementName}> requires '{name}' attribute");
        return attr.Value;
    }

    private static string GetAttr(XElement element, string name, string? defaultValue)
    {
        var attr = element.Attribute(name);
        return attr?.Value ?? defaultValue ?? "";
    }

    private static bool GetBoolAttr(XElement element, string name)
    {
        var attr = element.Attribute(name);
        if (attr == null) return false;
        return attr.Value.Equals("true", StringComparison.OrdinalIgnoreCase);
    }

    private static int GetIntAttr(XElement element, string name, int defaultValue)
    {
        var attr = element.Attribute(name);
        if (attr == null) return defaultValue;
        return int.TryParse(attr.Value, out var v) ? v : defaultValue;
    }

    private static int? GetNullableIntAttr(XElement element, string name)
    {
        var attr = element.Attribute(name);
        if (attr == null) return null;
        return int.TryParse(attr.Value, out var v) ? v : null;
    }

    private static void ParseColumnWidth(XElement element, ColumnDef col)
    {
        var attr = element.Attribute("width");
        if (attr == null) return;

        var value = attr.Value.Trim();
        if (value.EndsWith('%'))
        {
            if (double.TryParse(value[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var pct))
                col.WidthPercent = pct;
        }
        else
        {
            if (int.TryParse(value, out var w))
                col.Width = w;
        }
    }
}
