package engine

import (
	"fmt"
	"log"
	"strings"

	"golang.org/x/net/html"
)

// Injector handles content injection into HTML elements
type Injector struct {
	client      ContentClient
	siteID      string
	mdProcessor *MarkdownProcessor
}

// NewInjector creates a new content injector
func NewInjector(client ContentClient, siteID string) *Injector {
	return &Injector{
		client:      client,
		siteID:      siteID,
		mdProcessor: NewMarkdownProcessor(),
	}
}

// InjectContent replaces element content with database values and adds content IDs
func (i *Injector) InjectContent(element *Element, contentID string) error {
	// Fetch content from database/API
	contentItem, err := i.client.GetContent(i.siteID, contentID)
	if err != nil {
		return fmt.Errorf("fetching content for %s: %w", contentID, err)
	}

	// If no content found, keep original content but add data attributes
	if contentItem == nil {
		i.AddContentAttributes(element.Node, contentID, element.Type)
		return nil
	}

	// Replace element content based on type
	switch element.Type {
	case "text":
		i.injectTextContent(element.Node, contentItem.Value)
	case "markdown":
		i.injectMarkdownContent(element.Node, contentItem.Value)
	case "link":
		i.injectLinkContent(element.Node, contentItem.Value)
	default:
		i.injectTextContent(element.Node, contentItem.Value)
	}

	// Add data attributes for editor functionality
	i.AddContentAttributes(element.Node, contentID, element.Type)

	return nil
}

// InjectBulkContent efficiently injects multiple content items
func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
	// Extract content IDs for bulk fetch
	contentIDs := make([]string, len(elements))
	for idx, elem := range elements {
		contentIDs[idx] = elem.ContentID
	}

	// Bulk fetch content
	contentMap, err := i.client.GetBulkContent(i.siteID, contentIDs)
	if err != nil {
		return fmt.Errorf("bulk fetching content: %w", err)
	}

	// Inject each element
	for _, elem := range elements {
		contentItem, exists := contentMap[elem.ContentID]

		// Add content attributes regardless
		i.AddContentAttributes(elem.Element.Node, elem.ContentID, elem.Element.Type)

		if !exists {
			// Keep original content if not found in database
			continue
		}

		// Replace content based on type
		switch elem.Element.Type {
		case "text":
			i.injectTextContent(elem.Element.Node, contentItem.Value)
		case "markdown":
			i.injectMarkdownContent(elem.Element.Node, contentItem.Value)
		case "link":
			i.injectLinkContent(elem.Element.Node, contentItem.Value)
		default:
			i.injectTextContent(elem.Element.Node, contentItem.Value)
		}
	}

	return nil
}

// injectTextContent replaces text content in an element
func (i *Injector) injectTextContent(node *html.Node, content string) {
	// Remove all child nodes
	for child := node.FirstChild; child != nil; {
		next := child.NextSibling
		node.RemoveChild(child)
		child = next
	}

	// Add new text content
	textNode := &html.Node{
		Type: html.TextNode,
		Data: content,
	}
	node.AppendChild(textNode)
}

// injectMarkdownContent handles markdown content - converts markdown to HTML
func (i *Injector) injectMarkdownContent(node *html.Node, content string) {
	if content == "" {
		i.injectTextContent(node, "")
		return
	}

	// Convert markdown to HTML using server processor
	htmlContent, err := i.mdProcessor.ToHTML(content)
	if err != nil {
		log.Printf("⚠️ Markdown conversion failed for content '%s': %v, falling back to text", content, err)
		i.injectTextContent(node, content)
		return
	}

	// Inject the HTML content
	i.injectHTMLContent(node, htmlContent)
}

// injectLinkContent handles link/button content with URL extraction
func (i *Injector) injectLinkContent(node *html.Node, content string) {
	// For now, just inject the text content
	// TODO: Parse content for URL and text components
	i.injectTextContent(node, content)
}

// injectHTMLContent safely injects HTML content into a DOM node
// Preserves the original element and only replaces its content
func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) {
	// Clear existing content but preserve the element itself
	i.clearNode(node)

	if htmlContent == "" {
		return
	}

	// Wrap content for safe parsing
	wrappedHTML := "<div>" + htmlContent + "</div>"

	// Parse HTML string
	doc, err := html.Parse(strings.NewReader(wrappedHTML))
	if err != nil {
		log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err)
		i.injectTextContent(node, htmlContent)
		return
	}

	// Find the wrapper div and move its children to target node
	wrapper := i.findElementByTag(doc, "div")
	if wrapper == nil {
		log.Printf("Could not find wrapper div in parsed HTML")
		return
	}

	// Move parsed nodes to target element (preserving original element)
	for child := wrapper.FirstChild; child != nil; {
		next := child.NextSibling
		wrapper.RemoveChild(child)
		node.AppendChild(child)
		child = next
	}
}

// clearNode removes all child nodes from a given node
func (i *Injector) clearNode(node *html.Node) {
	for child := node.FirstChild; child != nil; {
		next := child.NextSibling
		node.RemoveChild(child)
		child = next
	}
}

// findElementByTag finds the first element with the specified tag name
func (i *Injector) findElementByTag(node *html.Node, tag string) *html.Node {
	if node.Type == html.ElementNode && node.Data == tag {
		return node
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if found := i.findElementByTag(child, tag); found != nil {
			return found
		}
	}

	return nil
}

// AddContentAttributes adds necessary data attributes and insertr class for editor functionality
func (i *Injector) AddContentAttributes(node *html.Node, contentID string, contentType string) {
	i.setAttribute(node, "data-content-id", contentID)
	i.setAttribute(node, "data-content-type", contentType)
	i.addClass(node, "insertr")
}

// InjectEditorAssets adds editor JavaScript to HTML document and injects demo gate if needed
func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, libraryScript string) {
	// Inject demo gate if no gates exist and add script for functionality
	if isDevelopment {
		i.InjectDemoGateIfNeeded(doc)
		i.InjectEditorScript(doc)
	}

	// TODO: Implement CDN script injection for production
	// Production options:
	// 1. Inject CDN script tag: <script src="https://cdn.jsdelivr.net/npm/@insertr/lib@1.0.0/dist/insertr.js"></script>
}

// findHeadElement finds the <head> element in the document
func (i *Injector) findHeadElement(node *html.Node) *html.Node {
	if node.Type == html.ElementNode && node.Data == "head" {
		return node
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if result := i.findHeadElement(child); result != nil {
			return result
		}
	}

	return nil
}

// setAttribute safely sets an attribute on an HTML node
func (i *Injector) setAttribute(node *html.Node, key, value string) {
	// Remove existing attribute if present
	for idx, attr := range node.Attr {
		if attr.Key == key {
			node.Attr = append(node.Attr[:idx], node.Attr[idx+1:]...)
			break
		}
	}

	// Add new attribute
	node.Attr = append(node.Attr, html.Attribute{
		Key: key,
		Val: value,
	})
}

// addClass safely adds a class to an HTML node
func (i *Injector) addClass(node *html.Node, className string) {
	var classAttr *html.Attribute
	var classIndex int = -1

	// Find existing class attribute
	for idx, attr := range node.Attr {
		if attr.Key == "class" {
			classAttr = &attr
			classIndex = idx
			break
		}
	}

	var classes []string
	if classAttr != nil {
		classes = strings.Fields(classAttr.Val)
	}

	// Check if class already exists
	for _, class := range classes {
		if class == className {
			return // Class already exists
		}
	}

	// Add new class
	classes = append(classes, className)
	newClassValue := strings.Join(classes, " ")

	if classIndex >= 0 {
		// Update existing class attribute
		node.Attr[classIndex].Val = newClassValue
	} else {
		// Add new class attribute
		node.Attr = append(node.Attr, html.Attribute{
			Key: "class",
			Val: newClassValue,
		})
	}
}

// Element represents a parsed HTML element with metadata
type Element struct {
	Node    *html.Node
	Type    string
	Tag     string
	Classes []string
	Content string
}

// ElementWithID combines an element with its generated content ID
type ElementWithID struct {
	Element   *Element
	ContentID string
}

// InjectDemoGateIfNeeded injects a demo gate element if no .insertr-gate elements exist
func (i *Injector) InjectDemoGateIfNeeded(doc *html.Node) {
	// Check if any .insertr-gate elements already exist
	if i.hasInsertrGate(doc) {
		return
	}

	// Find the body element
	bodyNode := i.findBodyElement(doc)
	if bodyNode == nil {
		log.Printf("Warning: Could not find body element to inject demo gate")
		return
	}

	// Create demo gate HTML structure
	gateHTML := `<div class="insertr-demo-gate" style="position: fixed; top: 20px; right: 20px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
    <button class="insertr-gate insertr-demo-gate-btn" style="background: #4f46e5; color: white; border: none; padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; user-select: none;" onmouseover="this.style.background='#4338ca'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(79, 70, 229, 0.4)'" onmouseout="this.style.background='#4f46e5'; this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(79, 70, 229, 0.3)'">
        <span style="font-size: 16px;">✏️</span>
        <span>Edit Site</span>
    </button>
</div>`

	// Parse the gate HTML and inject it into the body
	gateDoc, err := html.Parse(strings.NewReader(gateHTML))
	if err != nil {
		log.Printf("Error parsing demo gate HTML: %v", err)
		return
	}

	// Extract and inject the gate element
	if gateDiv := i.extractElementByClass(gateDoc, "insertr-demo-gate"); gateDiv != nil {
		if gateDiv.Parent != nil {
			gateDiv.Parent.RemoveChild(gateDiv)
		}
		bodyNode.AppendChild(gateDiv)
		log.Printf("✅ Demo gate injected: Edit button added to top-right corner")
	}
}

// InjectEditorScript injects the insertr.js library and initialization script
func (i *Injector) InjectEditorScript(doc *html.Node) {
	// Check if script is already injected
	if i.hasInsertrScript(doc) {
		return
	}

	// Find the head element for the script tag
	headNode := i.findHeadElement(doc)
	if headNode == nil {
		log.Printf("Warning: Could not find head element to inject editor script")
		return
	}

	// Create CSS and script elements that load from our server with site configuration
	insertrHTML := fmt.Sprintf(`<link rel="stylesheet" href="http://localhost:8080/insertr.css" data-insertr-injected="true">
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="%s" data-api-endpoint="http://localhost:8080/api/content" data-mock-auth="true" data-debug="true"></script>`, i.siteID)

	// Parse and inject the CSS and script elements
	insertrDoc, err := html.Parse(strings.NewReader(insertrHTML))
	if err != nil {
		log.Printf("Error parsing editor script HTML: %v", err)
		return
	}

	// Extract and inject all CSS and script elements
	if err := i.injectAllHeadElements(insertrDoc, headNode); err != nil {
		log.Printf("Error injecting CSS and script elements: %v", err)
		return
	}

	log.Printf("✅ Insertr.js library injected with site configuration")
}

// injectAllScriptElements finds and injects all script elements from parsed HTML
func (i *Injector) injectAllScriptElements(doc *html.Node, targetNode *html.Node) error {
	scripts := i.findAllScriptElements(doc)

	for _, script := range scripts {
		// Remove from original parent
		if script.Parent != nil {
			script.Parent.RemoveChild(script)
		}
		// Add to target node
		targetNode.AppendChild(script)
	}

	return nil
}

// findAllScriptElements recursively finds all script elements
func (i *Injector) findAllScriptElements(node *html.Node) []*html.Node {
	var scripts []*html.Node

	if node.Type == html.ElementNode && node.Data == "script" {
		scripts = append(scripts, node)
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		childScripts := i.findAllScriptElements(child)
		scripts = append(scripts, childScripts...)
	}

	return scripts
}

// injectAllHeadElements finds and injects all head elements (link, script) from parsed HTML
func (i *Injector) injectAllHeadElements(doc *html.Node, targetNode *html.Node) error {
	elements := i.findAllHeadElements(doc)

	for _, element := range elements {
		// Remove from original parent
		if element.Parent != nil {
			element.Parent.RemoveChild(element)
		}
		// Add to target node
		targetNode.AppendChild(element)
	}

	return nil
}

// findAllHeadElements recursively finds all link and script elements
func (i *Injector) findAllHeadElements(node *html.Node) []*html.Node {
	var elements []*html.Node

	if node.Type == html.ElementNode && (node.Data == "script" || node.Data == "link") {
		elements = append(elements, node)
	}

	for child := node.FirstChild; child != nil; child = child.NextSibling {
		childElements := i.findAllHeadElements(child)
		elements = append(elements, childElements...)
	}

	return elements
}

// hasInsertrGate checks if document has .insertr-gate elements
func (i *Injector) hasInsertrGate(node *html.Node) bool {
	if node.Type == html.ElementNode {
		for _, attr := range node.Attr {
			if attr.Key == "class" && strings.Contains(attr.Val, "insertr-gate") {
				return true
			}
		}
	}
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if i.hasInsertrGate(child) {
			return true
		}
	}
	return false
}

// hasInsertrScript checks if document already has insertr script injected
// Uses data-insertr-injected attribute for reliable detection that works with:
// - CDN URLs with version numbers (jsdelivr.net/npm/@insertr/lib@1.2.3/insertr.js)
// - Minified versions (insertr.min.js)
// - Query parameters (insertr.js?v=abc123)
// - Different CDN domains (unpkg.com, cdn.example.com)
func (i *Injector) hasInsertrScript(node *html.Node) bool {
	if node.Type == html.ElementNode && node.Data == "script" {
		for _, attr := range node.Attr {
			if attr.Key == "data-insertr-injected" {
				return true
			}
		}
	}
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if i.hasInsertrScript(child) {
			return true
		}
	}
	return false
}

// findBodyElement finds the <body> element
func (i *Injector) findBodyElement(node *html.Node) *html.Node {
	if node.Type == html.ElementNode && node.Data == "body" {
		return node
	}
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if result := i.findBodyElement(child); result != nil {
			return result
		}
	}
	return nil
}

// extractElementByClass finds element with specific class
func (i *Injector) extractElementByClass(node *html.Node, className string) *html.Node {
	if node.Type == html.ElementNode {
		for _, attr := range node.Attr {
			if attr.Key == "class" && strings.Contains(attr.Val, className) {
				return node
			}
		}
	}
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if result := i.extractElementByClass(child, className); result != nil {
			return result
		}
	}
	return nil
}

// extractElementByTag finds element with specific tag
func (i *Injector) extractElementByTag(node *html.Node, tagName string) *html.Node {
	if node.Type == html.ElementNode && node.Data == tagName {
		return node
	}
	for child := node.FirstChild; child != nil; child = child.NextSibling {
		if result := i.extractElementByTag(child, tagName); result != nil {
			return result
		}
	}
	return nil
}
