// Package server — JS module transpiler.
// Mirrors: odoo/tools/js_transpiler.py
//
// Converts ES module syntax (import/export) to odoo.define() format:
//
//	import { X } from "@web/foo"    -->  const { X } = require("@web/foo")
//	export class Foo { ... }        -->  const Foo = __exports.Foo = class Foo { ... }
//
// Wrapped in:
//
//	odoo.define("@web/core/foo", ["@web/foo"], function(require) {
//	    "use strict"; let __exports = {};
//	    ...
//	    return __exports;
//	});
package server

import (
	"fmt"
	"regexp"
	"strings"
)

// Compiled regex patterns for import/export matching.
// Mirrors: odoo/tools/js_transpiler.py URL_RE, IMPORT_RE, etc.
var (
	// Import patterns — (?m)^ ensures we only match at line start (not inside comments)
	reNamedImport    = regexp.MustCompile(`(?m)^\s*import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']\s*;?`)
	reDefaultImport  = regexp.MustCompile(`(?m)^\s*import\s+(\w+)\s+from\s*["']([^"']+)["']\s*;?`)
	reNamespaceImport = regexp.MustCompile(`(?m)^\s*import\s*\*\s*as\s+(\w+)\s+from\s*["']([^"']+)["']\s*;?`)
	reSideEffectImport = regexp.MustCompile(`(?m)^\s*import\s*["']([^"']+)["']\s*;?`)

	// Export patterns
	reExportClass    = regexp.MustCompile(`export\s+class\s+(\w+)`)
	reExportFunction = regexp.MustCompile(`export\s+(async\s+)?function\s+(\w+)`)
	reExportConst    = regexp.MustCompile(`export\s+const\s+(\w+)`)
	reExportLet      = regexp.MustCompile(`export\s+let\s+(\w+)`)
	reExportDefault  = regexp.MustCompile(`export\s+default\s+`)
	reExportNamedFrom = regexp.MustCompile(`export\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']\s*;?`)
	reExportNamed     = regexp.MustCompile(`export\s*\{([^}]*)\}\s*;?`)
	reExportStar     = regexp.MustCompile(`export\s*\*\s*from\s*["']([^"']+)["']\s*;?`)

	// Block comment removal
	reBlockComment = regexp.MustCompile(`(?s)/\*.*?\*/`)

	// Detection patterns
	reHasImport = regexp.MustCompile(`(?m)^\s*import\s`)
	reHasExport = regexp.MustCompile(`(?m)^\s*export\s`)
	reOdooModuleTag = regexp.MustCompile(`(?m)^\s*//\s*@odoo-module`)
	reOdooModuleIgnore = regexp.MustCompile(`(?m)^\s*//\s*@odoo-module\s+ignore`)
)

// TranspileJS converts an ES module JS file to odoo.define() format.
// Mirrors: odoo/tools/js_transpiler.py transpile_javascript()
//
// urlPath is the URL-style path, e.g. "/web/static/src/core/foo.js".
// content is the raw JS source code.
// Returns the transpiled source, or the original content if the file is not
// an ES module.
func TranspileJS(urlPath, content string) string {
	if !IsOdooModule(urlPath, content) {
		return content
	}

	moduleName := URLToModuleName(urlPath)

	// Extract imports and build dependency list
	deps, requireLines, cleanContent := extractImports(moduleName, content)

	// Transform exports
	cleanContent, exportedFuncs := transformExports(cleanContent)

	// Wrap in odoo.define
	return wrapWithOdooDefine(moduleName, deps, requireLines, cleanContent, exportedFuncs)
}

// URLToModuleName converts a URL path to an Odoo module name.
// Mirrors: odoo/tools/js_transpiler.py url_to_module_name()
//
// Examples:
//
//	/web/static/src/core/foo.js       -> @web/core/foo
//	/web/static/src/env.js            -> @web/env
//	/web/static/lib/luxon/luxon.js    -> @web/../lib/luxon/luxon
//	/stock/static/src/widgets/foo.js  -> @stock/widgets/foo
func URLToModuleName(url string) string {
	// Remove leading slash
	path := strings.TrimPrefix(url, "/")

	// Remove .js extension
	path = strings.TrimSuffix(path, ".js")

	// Split into addon name and the rest
	parts := strings.SplitN(path, "/", 2)
	if len(parts) < 2 {
		return "@" + path
	}

	addonName := parts[0]
	rest := parts[1]

	// Remove "static/src/" prefix from the rest
	if strings.HasPrefix(rest, "static/src/") {
		rest = strings.TrimPrefix(rest, "static/src/")
	} else if strings.HasPrefix(rest, "static/") {
		// For lib files: static/lib/foo -> ../lib/foo
		rest = "../" + strings.TrimPrefix(rest, "static/")
	}

	return "@" + addonName + "/" + rest
}

// resolveRelativeImport converts relative import paths to absolute module names.
// E.g., if current module is "@web/core/browser/feature_detection" and dep is "./browser",
// it resolves to "@web/core/browser/browser".
// "../utils/hooks" from "@web/core/browser/feature_detection" → "@web/core/utils/hooks"
func resolveRelativeImport(currentModule, dep string) string {
	if !strings.HasPrefix(dep, "./") && !strings.HasPrefix(dep, "../") {
		return dep // Already absolute
	}

	// Split current module into parts: @web/core/browser/feature_detection → [core, browser]
	// (remove the @addon/ prefix and the filename)
	parts := strings.Split(currentModule, "/")
	if len(parts) < 2 {
		return dep
	}

	// Get the directory of the current module (drop the last segment = filename)
	dir := parts[:len(parts)-1]

	// Resolve the relative path
	relParts := strings.Split(dep, "/")
	for _, p := range relParts {
		if p == "." {
			continue
		} else if p == ".." {
			if len(dir) > 1 {
				dir = dir[:len(dir)-1]
			}
		} else {
			dir = append(dir, p)
		}
	}

	return strings.Join(dir, "/")
}

// IsOdooModule determines whether a JS file should be transpiled.
// Mirrors: odoo/tools/js_transpiler.py is_odoo_module()
//
// Returns true if the file contains ES module syntax (import/export) or
// the @odoo-module tag (without "ignore").
func IsOdooModule(url, content string) bool {
	// Must be a JS file
	if !strings.HasSuffix(url, ".js") {
		return false
	}

	// Explicit ignore directive
	if reOdooModuleIgnore.MatchString(content) {
		return false
	}

	// Explicit @odoo-module tag
	if reOdooModuleTag.MatchString(content) {
		return true
	}

	// Has import or export statements
	if reHasImport.MatchString(content) || reHasExport.MatchString(content) {
		return true
	}

	return false
}

// extractImports finds all import statements in the content, returns:
//   - deps: list of dependency module names (for the odoo.define deps array)
//   - requireLines: list of "const ... = require(...)" lines
//   - cleanContent: content with import statements removed
func extractImports(moduleName, content string) (deps []string, requireLines []string, cleanContent string) {
	depSet := make(map[string]bool)
	var depOrder []string

	resolve := func(dep string) string {
		return resolveRelativeImport(moduleName, dep)
	}

	addDep := func(dep string) {
		dep = resolve(dep)
		if !depSet[dep] {
			depSet[dep] = true
			depOrder = append(depOrder, dep)
		}
	}

	cleanContent = content

	// Remove @odoo-module tag line (not needed in output)
	cleanContent = reOdooModuleTag.ReplaceAllString(cleanContent, "")

	// Don't strip block comments (it breaks string literals containing /*).
	// Instead, the import regexes below only match at positions that are
	// clearly actual code, not inside comments. Since import/export statements
	// in ES modules must appear at the top level (before any function body),
	// they'll always be at the beginning of a line. The regexes already handle
	// this correctly for most cases. The one edge case (import inside JSDoc)
	// is handled by checking the matched line doesn't start with * or //.

	// Named imports: import { X, Y as Z } from "dep"
	cleanContent = reNamedImport.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reNamedImport.FindStringSubmatch(match)
		if len(m) < 3 {
			return match
		}
		names := m[1]
		dep := m[2]
		addDep(dep)

		// Parse the import specifiers, handle "as" aliases
		specifiers := parseImportSpecifiers(names)
		if len(specifiers) == 0 {
			return ""
		}

		// Build destructuring: const { X, Y: Z } = require("dep")
		var parts []string
		for _, s := range specifiers {
			if s.alias != "" {
				parts = append(parts, s.name+": "+s.alias)
			} else {
				parts = append(parts, s.name)
			}
		}
		line := "const { " + strings.Join(parts, ", ") + " } = require(\"" + resolve(dep) + "\");"
		requireLines = append(requireLines, line)
		return ""
	})

	// Namespace imports: import * as X from "dep"
	cleanContent = reNamespaceImport.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reNamespaceImport.FindStringSubmatch(match)
		if len(m) < 3 {
			return match
		}
		name := m[1]
		dep := m[2]
		addDep(dep)

		line := "const " + name + " = require(\"" + dep + "\");"
		requireLines = append(requireLines, line)
		return ""
	})

	// Default imports: import X from "dep"
	cleanContent = reDefaultImport.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reDefaultImport.FindStringSubmatch(match)
		if len(m) < 3 {
			return match
		}
		name := m[1]
		dep := m[2]
		addDep(dep)

		// Default import uses Symbol.for("default")
		line := "const " + name + " = require(\"" + dep + "\")[Symbol.for(\"default\")];"
		requireLines = append(requireLines, line)
		return ""
	})

	// Side-effect imports: import "dep"
	cleanContent = reSideEffectImport.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reSideEffectImport.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		dep := m[1]
		addDep(dep)

		line := "require(\"" + dep + "\");"
		requireLines = append(requireLines, line)
		return ""
	})

	// export { X, Y } from "dep" — named re-export: import dep + export names
	cleanContent = reExportNamedFrom.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reExportNamedFrom.FindStringSubmatch(match)
		if len(m) >= 3 {
			names := m[1]
			dep := resolve(m[2])
			addDep(m[2])
			// Named re-export: export { X } from "dep"
			// Import the dep (using a temp var to avoid redeclaration with existing imports)
			// then assign to __exports
			specifiers := parseExportSpecifiers(names)
			var parts []string
			tmpVar := fmt.Sprintf("_reexport_%d", len(deps))
			parts = append(parts, fmt.Sprintf("var %s = require(\"%s\");", tmpVar, dep))
			for _, s := range specifiers {
				exported := s.name
				if s.alias != "" {
					exported = s.alias
				}
				parts = append(parts, fmt.Sprintf("__exports.%s = %s.%s;", exported, tmpVar, s.name))
			}
			return strings.Join(parts, " ")
		}
		return match
	})

	// export * from "dep" — treat as import dependency
	cleanContent = reExportStar.ReplaceAllStringFunc(cleanContent, func(match string) string {
		m := reExportStar.FindStringSubmatch(match)
		if len(m) >= 2 {
			addDep(m[1])
		}
		return match // keep the export * line — transformExports will handle it
	})

	deps = depOrder
	return
}

// importSpecifier holds a single import specifier, e.g. "X" or "X as Y".
type importSpecifier struct {
	name  string // exported name
	alias string // local alias (empty if same as name)
}

// parseImportSpecifiers parses the inside of { ... } in an import statement.
// E.g. "X, Y as Z, W" -> [{X, ""}, {Y, "Z"}, {W, ""}]
func parseImportSpecifiers(raw string) []importSpecifier {
	var result []importSpecifier
	for _, part := range strings.Split(raw, ",") {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		fields := strings.Fields(part)
		switch len(fields) {
		case 1:
			result = append(result, importSpecifier{name: fields[0]})
		case 3:
			// "X as Y"
			if fields[1] == "as" {
				result = append(result, importSpecifier{name: fields[0], alias: fields[2]})
			}
		}
	}
	return result
}

// transformExports converts export statements to __exports assignments.
// Returns the transformed content and a list of exported function names
// (these need deferred __exports assignment to preserve hoisting).
// Mirrors: odoo/tools/js_transpiler.py (various export transformers)
func transformExports(content string) (string, []string) {
	// export class Foo { ... }  ->  const Foo = __exports.Foo = class Foo { ... }
	content = reExportClass.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportClass.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		name := m[1]
		return "const " + name + " = __exports." + name + " = class " + name
	})

	// export [async] function foo(...) { ... }
	// Must preserve function declaration hoisting (not var/const) because some
	// Odoo modules reference the function before its declaration line
	// (e.g., rpc.setCache assigned before "export function rpc").
	// Strategy: keep as hoisted function declaration, collect names, append
	// __exports assignments at the end.
	var exportedFuncNames []string
	content = reExportFunction.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportFunction.FindStringSubmatch(match)
		if len(m) < 3 {
			return match
		}
		async := m[1] // "async " or ""
		name := m[2]
		exportedFuncNames = append(exportedFuncNames, name)
		return async + "function " + name
	})

	// export const foo = ...  ->  const foo = __exports.foo = ...
	// (replaces just the "export const foo" part, the rest of the line stays)
	content = reExportConst.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportConst.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		name := m[1]
		return "const " + name + " = __exports." + name
	})

	// export let foo = ...  ->  let foo = __exports.foo = ...
	content = reExportLet.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportLet.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		name := m[1]
		return "let " + name + " = __exports." + name
	})

	// export { X, Y, Z }  ->  Object.assign(__exports, { X, Y, Z });
	content = reExportNamed.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportNamed.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		names := m[1]

		// Parse individual names, handle "X as Y" aliases
		specifiers := parseExportSpecifiers(names)
		if len(specifiers) == 0 {
			return ""
		}

		var assignments []string
		for _, s := range specifiers {
			exportedName := s.name
			if s.alias != "" {
				exportedName = s.alias
			}
			assignments = append(assignments, "__exports."+exportedName+" = "+s.name+";")
		}
		return strings.Join(assignments, " ")
	})

	// export * from "dep"  ->  Object.assign(__exports, require("dep"))
	// Also add the dep to the dependency list (handled in extractImports)
	content = reExportStar.ReplaceAllStringFunc(content, func(match string) string {
		m := reExportStar.FindStringSubmatch(match)
		if len(m) < 2 {
			return match
		}
		dep := m[1]
		return fmt.Sprintf(`Object.assign(__exports, require("%s"))`, dep)
	})

	// export default X  ->  __exports[Symbol.for("default")] = X
	// Must come after other export patterns to avoid double-matching
	content = reExportDefault.ReplaceAllString(content, `__exports[Symbol.for("default")] = `)

	return content, exportedFuncNames
}

// exportSpecifier holds a single export specifier from "export { X, Y as Z }".
type exportSpecifier struct {
	name  string // local name
	alias string // exported name (empty if same as name)
}

// parseExportSpecifiers parses the inside of { ... } in an export statement.
// E.g. "X, Y as Z" -> [{X, ""}, {Y, "Z"}]
func parseExportSpecifiers(raw string) []exportSpecifier {
	var result []exportSpecifier
	for _, part := range strings.Split(raw, ",") {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		fields := strings.Fields(part)
		switch len(fields) {
		case 1:
			result = append(result, exportSpecifier{name: fields[0]})
		case 3:
			// "X as Y"
			if fields[1] == "as" {
				result = append(result, exportSpecifier{name: fields[0], alias: fields[2]})
			}
		}
	}
	return result
}

// wrapWithOdooDefine wraps the transpiled content in an odoo.define() call.
// Mirrors: odoo/tools/js_transpiler.py wrap_with_odoo_define()
func wrapWithOdooDefine(moduleName string, deps []string, requireLines []string, content string, exportedFuncs []string) string {
	var b strings.Builder

	// Module definition header
	b.WriteString("odoo.define(\"")
	b.WriteString(moduleName)
	b.WriteString("\", [")

	// Dependencies array
	for i, dep := range deps {
		if i > 0 {
			b.WriteString(", ")
		}
		b.WriteString("\"")
		b.WriteString(dep)
		b.WriteString("\"")
	}

	b.WriteString("], function(require) {\n")
	b.WriteString("\"use strict\";\n")
	b.WriteString("let __exports = {};\n")

	// Require statements
	for _, line := range requireLines {
		b.WriteString(line)
		b.WriteString("\n")
	}

	// Original content (trimmed of leading/trailing whitespace)
	trimmed := strings.TrimSpace(content)
	if trimmed != "" {
		b.WriteString(trimmed)
		b.WriteString("\n")
	}

	// Deferred __exports assignments for hoisted function declarations
	for _, name := range exportedFuncs {
		b.WriteString("__exports." + name + " = " + name + ";\n")
	}

	// Return exports
	b.WriteString("return __exports;\n")
	b.WriteString("});\n")

	return b.String()
}

// stripJSDocImports removes import/export statements that appear inside JSDoc
// block comments. Instead of stripping all /* ... */ (which breaks string literals
// containing /*), we only neutralize import/export lines that are preceded by
// a JSDoc comment start (/**) on a prior line. We detect this by checking if
// the line is inside a comment block.
func stripJSDocImports(content string) string {
	lines := strings.Split(content, "\n")
	inComment := false
	var result []string

	for _, line := range lines {
		trimmed := strings.TrimSpace(line)

		// Track block comment state
		if strings.HasPrefix(trimmed, "/*") {
			inComment = true
		}

		if inComment {
			// Neutralize import/export statements inside comments
			// by replacing 'import' with '_import' and 'export' with '_export'
			if strings.Contains(trimmed, "import ") || strings.Contains(trimmed, "export ") {
				line = strings.Replace(line, "import ", "_import_in_comment ", 1)
				line = strings.Replace(line, "export ", "_export_in_comment ", 1)
			}
		}

		if strings.Contains(trimmed, "*/") {
			inComment = false
		}

		result = append(result, line)
	}

	return strings.Join(result, "\n")
}
