package server

import (
	"encoding/xml"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
)

// compileXMLTemplates reads all XML template files listed in assets_xml.txt,
// parses them, and generates a JS module with registerTemplate() /
// registerTemplateExtension() calls -- the same output that
// tools/compile_templates.py used to produce at build time.
//
// Mirrors: odoo/addons/base/models/assetsbundle.py generate_xml_bundle()
func compileXMLTemplates(frontendDir string) string {
	// xmlFiles is populated by init() in webclient.go from the embedded
	// assets_xml.txt file.

	// Collect blocks of templates and extensions to preserve ordering
	// semantics that Odoo relies on (primary templates first, then
	// extensions, interleaved per-file).
	type primaryEntry struct {
		name        string
		url         string
		xmlStr      string
		inheritFrom string
	}
	type extensionEntry struct {
		inheritFrom string
		url         string
		xmlStr      string
	}
	type block struct {
		kind       string // "templates" or "extensions"
		templates  []primaryEntry
		extensions []extensionEntry
	}

	var blocks []block
	var curBlock *block
	templateCount := 0
	errorCount := 0

	for _, urlPath := range xmlFiles {
		content := readFileFromFrontend(frontendDir, urlPath)
		if content == "" {
			continue
		}

		// Parse the XML: we need the immediate children of the root element
		// (typically <templates>). Each child with t-name is a primary
		// template; each with t-inherit + t-inherit-mode="extension" is an
		// extension.
		elements, err := parseXMLTemplateFile(content)
		if err != nil {
			log.Printf("templates: ERROR parsing %s: %v", urlPath, err)
			errorCount++
			continue
		}

		for _, elem := range elements {
			tName := elemAttr(elem, "t-name")
			tInherit := elemAttr(elem, "t-inherit")
			tInheritMode := ""
			if tInherit != "" {
				tInheritMode = elemAttr(elem, "t-inherit-mode")
				if tInheritMode == "" {
					tInheritMode = "primary"
				}
				if tInheritMode != "primary" && tInheritMode != "extension" {
					log.Printf("templates: ERROR invalid inherit mode %q in %s template %s", tInheritMode, urlPath, tName)
					errorCount++
					continue
				}
			}

			serialized := serializeElement(elem)

			if tInheritMode == "extension" {
				if curBlock == nil || curBlock.kind != "extensions" {
					blocks = append(blocks, block{kind: "extensions"})
					curBlock = &blocks[len(blocks)-1]
				}
				curBlock.extensions = append(curBlock.extensions, extensionEntry{
					inheritFrom: tInherit,
					url:         urlPath,
					xmlStr:      serialized,
				})
				templateCount++
			} else if tName != "" {
				if curBlock == nil || curBlock.kind != "templates" {
					blocks = append(blocks, block{kind: "templates"})
					curBlock = &blocks[len(blocks)-1]
				}
				curBlock.templates = append(curBlock.templates, primaryEntry{
					name:        tName,
					url:         urlPath,
					xmlStr:      serialized,
					inheritFrom: tInherit,
				})
				templateCount++
			}
			// Elements without t-name and without extension mode are skipped.
		}
	}

	// Generate JS registerTemplate / registerTemplateExtension calls.
	var content []string
	names := map[string]bool{}
	primaryParents := map[string]bool{}
	extensionParents := map[string]bool{}

	for i := range blocks {
		blk := &blocks[i]
		if blk.kind == "templates" {
			for _, t := range blk.templates {
				if t.inheritFrom != "" {
					primaryParents[t.inheritFrom] = true
				}
				names[t.name] = true
				escaped := escapeForJSTemplateLiteral(t.xmlStr)
				content = append(content, fmt.Sprintf("registerTemplate(%q, `%s`, `%s`);",
					t.name, t.url, escaped))
			}
		} else {
			for _, ext := range blk.extensions {
				extensionParents[ext.inheritFrom] = true
				escaped := escapeForJSTemplateLiteral(ext.xmlStr)
				content = append(content, fmt.Sprintf("registerTemplateExtension(%q, `%s`, `%s`);",
					ext.inheritFrom, ext.url, escaped))
			}
		}
	}

	// Check for missing parent templates (diagnostic aid).
	var missingPrimary []string
	for p := range primaryParents {
		if !names[p] {
			missingPrimary = append(missingPrimary, p)
		}
	}
	if len(missingPrimary) > 0 {
		content = append(content, fmt.Sprintf("checkPrimaryTemplateParents(%s);", jsonStringArray(missingPrimary)))
	}

	var missingExtension []string
	for p := range extensionParents {
		if !names[p] {
			missingExtension = append(missingExtension, p)
		}
	}
	if len(missingExtension) > 0 {
		content = append(content, fmt.Sprintf("console.error(\"Missing (extension) parent templates: %s\");",
			strings.Join(missingExtension, ", ")))
	}

	// Wrap in odoo.define module.
	var buf strings.Builder
	buf.WriteString("odoo.define(\"@web/bundle_xml\", [\"@web/core/templates\"], function(require) {\n")
	buf.WriteString("    \"use strict\";\n")
	buf.WriteString("    const { checkPrimaryTemplateParents, registerTemplate, registerTemplateExtension } = require(\"@web/core/templates\");\n")
	buf.WriteString("\n")
	for _, line := range content {
		buf.WriteString(line)
		buf.WriteString("\n")
	}
	buf.WriteString("});\n")

	log.Printf("templates: %d templates compiled, %d errors", templateCount, errorCount)
	return buf.String()
}

// readFileFromFrontend reads a file from the frontend assets directory.
func readFileFromFrontend(frontendDir string, urlPath string) string {
	if frontendDir == "" {
		return ""
	}
	rel := strings.TrimPrefix(urlPath, "/")
	fullPath := filepath.Join(frontendDir, rel)
	data, err := os.ReadFile(fullPath)
	if err == nil {
		return string(data)
	}
	return ""
}

// --- XML parsing ---------------------------------------------------------
//
// Strategy: we use encoding/xml.Decoder only to identify where each direct
// child of the root element starts and ends (via InputOffset). Then we
// extract the raw substring from the original file content. This preserves
// whitespace, attribute order, namespace prefixes, and all other details
// exactly as they appear in the source file -- critical for OWL template
// fidelity.
//
// After extraction we strip XML comments from the raw snippet (matching
// Python lxml's remove_comments=True) and inject the xml:space="preserve"
// attribute into the root element of the snippet (matching Python lxml's
// etree.tostring behavior after element.set(xml:space, "preserve")).

// xmlElement represents a parsed direct child of the XML root.
type xmlElement struct {
	raw   string            // the serialized XML string of the element
	attrs map[string]string // element attributes (for t-name, t-inherit, etc.)
}

func elemAttr(e xmlElement, name string) string {
	return e.attrs[name]
}

// parseXMLTemplateFile extracts the direct children of the root element.
// It returns each child as an xmlElement with its raw XML and attributes.
func parseXMLTemplateFile(fileContent string) ([]xmlElement, error) {
	decoder := xml.NewDecoder(strings.NewReader(fileContent))
	// Odoo templates use custom attribute names (t-name, t-if, etc.) that
	// are valid XML but may trigger namespace warnings. Use non-strict
	// mode and provide an entity map for common HTML entities.
	decoder.Strict = false
	decoder.Entity = defaultXMLEntities()

	var elements []xmlElement
	depth := 0 // 0 = outside root, 1 = inside root, 2+ = inside child

	// childStartOffset: byte offset just before the '<' of the child's
	// opening tag.
	childStartOffset := int64(0)
	childDepth := 0
	inChild := false
	var childAttrs map[string]string

	for {
		// Capture offset before consuming token. InputOffset reports the
		// byte position of the NEXT byte the decoder will read, so we
		// record it before calling Token() to get the start of each token.
		offset := decoder.InputOffset()
		tok, err := decoder.Token()
		if err != nil {
			break // EOF or error
		}

		switch t := tok.(type) {
		case xml.StartElement:
			depth++
			if depth == 2 && !inChild {
				// Start of a direct child of root
				inChild = true
				childStartOffset = offset
				childDepth = 1
				childAttrs = make(map[string]string)
				for _, a := range t.Attr {
					key := a.Name.Local
					if a.Name.Space != "" {
						key = a.Name.Space + ":" + key
					}
					childAttrs[key] = a.Value
				}
			} else if inChild {
				childDepth++
			}
		case xml.EndElement:
			if inChild {
				childDepth--
				if childDepth == 0 {
					// End of direct child -- extract raw XML.
					endOffset := decoder.InputOffset()
					raw := fileContent[childStartOffset:endOffset]

					// Strip XML comments (<!-- ... -->)
					raw = stripXMLComments(raw)

					// Inject xml:space="preserve" into root element
					raw = injectXmlSpacePreserve(raw)

					elements = append(elements, xmlElement{
						raw:   raw,
						attrs: childAttrs,
					})
					inChild = false
					childAttrs = nil
				}
			}
			depth--
		}
	}

	return elements, nil
}

// stripXMLComments removes all XML comments from a string.
func stripXMLComments(s string) string {
	for {
		start := strings.Index(s, "<!--")
		if start < 0 {
			break
		}
		end := strings.Index(s[start:], "-->")
		if end < 0 {
			break
		}
		s = s[:start] + s[start+end+3:]
	}
	return s
}

// injectXmlSpacePreserve adds xml:space="preserve" to the first opening
// tag in the string, unless it already has that attribute.
func injectXmlSpacePreserve(s string) string {
	if strings.Contains(s, `xml:space=`) {
		return s
	}
	// Find the end of the first opening tag (the first '>' that isn't
	// inside an attribute value).
	idx := findFirstTagClose(s)
	if idx < 0 {
		return s
	}
	// For self-closing tags like <t t-name="foo"/>, insert before the '/'
	if idx > 0 && s[idx-1] == '/' {
		return s[:idx-1] + ` xml:space="preserve"` + s[idx-1:]
	}
	return s[:idx] + ` xml:space="preserve"` + s[idx:]
}

// findFirstTagClose returns the index of the '>' that closes the first
// start tag in the string, correctly skipping over '>' characters inside
// attribute values.
func findFirstTagClose(s string) int {
	inAttr := false
	attrQuote := byte(0)
	started := false
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !started {
			if c == '<' {
				started = true
			}
			continue
		}
		if inAttr {
			if c == attrQuote {
				inAttr = false
			}
			continue
		}
		if c == '"' || c == '\'' {
			inAttr = true
			attrQuote = c
			continue
		}
		if c == '>' {
			return i
		}
	}
	return -1
}

// defaultXMLEntities returns a map of common HTML entities that may appear
// in Odoo templates (encoding/xml only knows the 5 XML built-in entities).
func defaultXMLEntities() map[string]string {
	return map[string]string{
		"nbsp": "\u00A0",
		"lt":   "<",
		"gt":   ">",
		"amp":  "&",
		"quot": "\"",
		"apos": "'",
	}
}

// --- JS escaping ---------------------------------------------------------

// escapeForJSTemplateLiteral escapes a string for use inside a JS template
// literal (backtick-delimited string).
func escapeForJSTemplateLiteral(s string) string {
	s = strings.ReplaceAll(s, "\\", "\\\\")
	s = strings.ReplaceAll(s, "`", "\\`")
	s = strings.ReplaceAll(s, "${", "\\${")
	return s
}

// --- Helpers -------------------------------------------------------------

// serializeElement returns the raw XML string of an xmlElement.
func serializeElement(e xmlElement) string {
	return e.raw
}

// jsonStringArray returns a JSON-encoded array of strings (simple, no
// external dependency needed).
func jsonStringArray(items []string) string {
	var parts []string
	for _, s := range items {
		escaped := strings.ReplaceAll(s, "\\", "\\\\")
		escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
		parts = append(parts, "\""+escaped+"\"")
	}
	return "[" + strings.Join(parts, ", ") + "]"
}
