package agent

import (
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/yaoapp/gou/application"
	v8 "github.com/yaoapp/gou/runtime/v8"
	"github.com/yaoapp/kun/log"
	"github.com/yaoapp/yao/sui/core"
	"golang.org/x/text/language"
)

// Template is the struct for the agent sui template
type Template struct {
	Root    string `json:"-"`
	agent   *Agent
	locales []core.SelectOption
	loaded  map[string]core.IPage
	*core.Template
}

// Pages get the pages from both /agent/pages and /assistants/*/pages
func (tmpl *Template) Pages() ([]core.IPage, error) {
	pages := []core.IPage{}

	// 1. Get pages from /agent/pages (global agent pages like login, error, etc.)
	agentPagesDir := filepath.Join(tmpl.agent.root, "pages")
	if tmpl.agent.fs.IsDir(agentPagesDir) {
		agentPages, err := tmpl.getPagesFromDir(agentPagesDir, "")
		if err != nil {
			log.Error("[Agent] Failed to load agent pages: %v", err)
		} else {
			pages = append(pages, agentPages...)
		}
	}

	// 2. Get pages from each assistant's pages directory
	assistants, err := tmpl.agent.getAssistants()
	if err != nil {
		return nil, err
	}

	for _, assistantID := range assistants {
		pagesDir := tmpl.agent.getAssistantPagesRoot(assistantID)
		assistantPages, err := tmpl.getPagesFromDir(pagesDir, assistantID)
		if err != nil {
			log.Error("[Agent] Failed to load pages for assistant %s: %v", assistantID, err)
			continue
		}
		pages = append(pages, assistantPages...)
	}

	return pages, nil
}

// getPagesFromDir get pages from a directory with optional route prefix
func (tmpl *Template) getPagesFromDir(dir string, routePrefix string) ([]core.IPage, error) {
	exts := []string{"*.sui", "*.html", "*.htm", "*.page"}
	pages := []core.IPage{}

	tmpl.agent.fs.Walk(dir, func(root, file string, isdir bool) error {
		name := filepath.Base(file)
		if isdir {
			if strings.HasPrefix(name, "__") || name == ".tmp" {
				return filepath.SkipDir
			}
			return nil
		}

		if strings.HasPrefix(name, "__") {
			return nil
		}

		page, err := tmpl.getPageFrom(file, dir, routePrefix)
		if err != nil {
			log.Error("[Agent] Get page error: %v", err)
			return nil
		}

		pages = append(pages, page)
		return nil
	}, exts...)

	return pages, nil
}

// getPageFrom create a page from file
func (tmpl *Template) getPageFrom(file, pagesRoot, assistantID string) (core.IPage, error) {
	route := tmpl.getPageRoute(file, pagesRoot, assistantID)
	return tmpl.getPage(route, file, pagesRoot, assistantID)
}

// getPageRoute get the route for a page
func (tmpl *Template) getPageRoute(file, pagesRoot, assistantID string) string {
	// Get relative path from pages root
	relPath := filepath.Dir(file[len(pagesRoot):])

	// Add assistant prefix if this is an assistant page
	if assistantID != "" {
		return filepath.Join("/", assistantID, relPath)
	}

	return relPath
}

// getPage create a page object
func (tmpl *Template) getPage(route, file, pagesRoot, assistantID string) (core.IPage, error) {
	path := filepath.Dir(file)
	name := tmpl.getPageBase(route)

	return &Page{
		Page: &core.Page{
			Route:      route,
			Path:       path,
			Name:       name,
			TemplateID: tmpl.ID,
			SuiID:      tmpl.agent.DSL.ID,
			Codes: core.SourceCodes{
				HTML: core.Source{File: fmt.Sprintf("%s%s", name, filepath.Ext(file))},
				CSS:  core.Source{File: fmt.Sprintf("%s.css", name)},
				JS:   core.Source{File: fmt.Sprintf("%s.js", name)},
				DATA: core.Source{File: fmt.Sprintf("%s.json", name)},
				TS:   core.Source{File: fmt.Sprintf("%s.ts", name)},
				LESS: core.Source{File: fmt.Sprintf("%s.less", name)},
				CONF: core.Source{File: fmt.Sprintf("%s.config", name)},
			},
		},
		tmpl:        tmpl,
		assistantID: assistantID,
		pagesRoot:   pagesRoot,
	}, nil
}

func (tmpl *Template) getPageBase(route string) string {
	return filepath.Base(route)
}

// Page get a specific page by route
// Route format: "/assistant-id/page-path" where assistant-id can contain dots for nested assistants
// Examples:
//   - "/expense/test" -> assistant "expense", page "/test"
//   - "/tests.nested.demo/article" -> assistant "tests.nested.demo", page "/article"
//   - "/index" -> agent page (no assistant prefix)
func (tmpl *Template) Page(route string) (core.IPage, error) {
	// Parse the route to determine if it's an assistant page or agent page
	parts := strings.Split(strings.Trim(route, "/"), "/")

	if len(parts) == 0 {
		return nil, fmt.Errorf("Invalid route: %s", route)
	}

	// Check if first part is an assistant ID (may contain dots for nested assistants)
	assistants, err := tmpl.agent.getAssistants()
	if err != nil {
		return nil, err
	}

	assistantID := ""
	pageRoute := route
	pagesRoot := filepath.Join(tmpl.agent.root, "pages")

	// The first part of the route might be an assistant ID
	// Assistant IDs can contain dots (e.g., "tests.nested.demo")
	for _, ast := range assistants {
		if parts[0] == ast {
			assistantID = ast
			pageRoute = "/" + strings.Join(parts[1:], "/")
			pagesRoot = tmpl.agent.getAssistantPagesRoot(assistantID)
			break
		}
	}

	// Find the page file
	pagePath := tmpl.getPagePath(pageRoute, pagesRoot)
	exts := []string{".sui", ".html", ".htm", ".page"}

	for _, ext := range exts {
		file := fmt.Sprintf("%s%s", pagePath, ext)
		if tmpl.agent.fs.IsFile(file) {
			return tmpl.getPage(route, file, pagesRoot, assistantID)
		}
	}

	return nil, fmt.Errorf("Page not found: %s", route)
}

func (tmpl *Template) getPagePath(route, pagesRoot string) string {
	name := tmpl.getPageBase(route)
	return filepath.Join(pagesRoot, route, name)
}

// PageExist check if page exists
func (tmpl *Template) PageExist(route string) bool {
	_, err := tmpl.Page(route)
	return err == nil
}

// RemovePage remove a page (not supported)
func (tmpl *Template) RemovePage(route string) error {
	return fmt.Errorf("RemovePage is not supported for agent pages")
}

// GetPageFromAsset get page from asset
func (tmpl *Template) GetPageFromAsset(file string) (core.IPage, error) {
	route := filepath.Dir(file)
	return tmpl.Page(route)
}

// CreateEmptyPage create an empty page (not supported)
func (tmpl *Template) CreateEmptyPage(route string, setting *core.PageSetting) (core.IPage, error) {
	return nil, fmt.Errorf("CreateEmptyPage is not supported for agent pages")
}

// CreatePage create a page from source (not supported for editing)
func (tmpl *Template) CreatePage(source string) core.IPage {
	// This is used for rendering, we need to find the page by route
	page, err := tmpl.Page(source)
	if err != nil {
		log.Error("[Agent] CreatePage error: %v", err)
		return nil
	}
	return page
}

// GetRoot get the root path (returns agent root for assets, etc.)
func (tmpl *Template) GetRoot() string {
	return tmpl.agent.root
}

// GetWatchDirs returns all directories that should be watched for changes
// This implements the core.IWatchDirs interface
func (tmpl *Template) GetWatchDirs() []string {
	dirs := []string{}

	// 1. Add the main agent template directory
	dirs = append(dirs, tmpl.agent.root)

	// 2. Add each assistant's pages directory
	assistants, err := tmpl.agent.getAssistants()
	if err != nil {
		return dirs
	}

	for _, assistantID := range assistants {
		pagesDir := tmpl.agent.getAssistantPagesRoot(assistantID)
		dirs = append(dirs, pagesDir)
	}

	return dirs
}

// GetWatchRoot returns "app" to indicate paths are relative to application source root
func (tmpl *Template) GetWatchRoot() string {
	return "app"
}

// Asset get the asset (check agent assets first, then assistant assets)
func (tmpl *Template) Asset(file string, width, height uint) (*core.Asset, error) {
	// First check in agent assets
	agentFile := filepath.Join(tmpl.agent.root, "__assets", file)
	if tmpl.agent.fs.IsFile(agentFile) {
		return tmpl.readAsset(agentFile, width, height)
	}

	// If not found and this is an assistant-specific request, check assistant assets
	// Format: /<assistant-id>/assets/...
	parts := strings.SplitN(strings.TrimPrefix(file, "/"), "/", 2)
	if len(parts) >= 2 {
		assistantID := parts[0]
		assetPath := parts[1]

		// Check if this is a valid assistant
		assistants, _ := tmpl.agent.getAssistants()
		for _, ast := range assistants {
			if ast == assistantID {
				assistantAssetFile := filepath.Join(tmpl.agent.assistantsRoot, assistantID, "pages", "__assets", assetPath)
				if tmpl.agent.fs.IsFile(assistantAssetFile) {
					return tmpl.readAsset(assistantAssetFile, width, height)
				}
				break
			}
		}
	}

	return nil, fmt.Errorf("Asset %s not found", file)
}

// readAsset read asset from file
func (tmpl *Template) readAsset(file string, width, height uint) (*core.Asset, error) {
	content, err := tmpl.agent.fs.ReadFile(file)
	if err != nil {
		return nil, err
	}

	typ, err := tmpl.agent.fs.MimeType(file)
	if err != nil {
		typ = "application/octet-stream"
	}

	return &core.Asset{Type: typ, Content: content}, nil
}

// Locales get the global locales
func (tmpl *Template) Locales() []core.SelectOption {
	if tmpl.locales != nil {
		return tmpl.locales
	}

	supportLocales := []core.SelectOption{}
	localeMap := map[string]bool{}

	// Check __locales directory
	path := filepath.Join(tmpl.Root, "__locales")
	if !tmpl.agent.fs.IsDir(path) {
		return supportLocales
	}

	dirs, err := tmpl.agent.fs.ReadDir(path, false)
	if err != nil {
		return supportLocales
	}

	for _, dir := range dirs {
		locale := filepath.Base(dir)
		if localeMap[locale] {
			continue
		}
		label := language.Make(locale).String()
		localeMap[locale] = true
		supportLocales = append(supportLocales, core.SelectOption{
			Value: locale,
			Label: label,
		})
	}

	tmpl.locales = supportLocales
	return tmpl.locales
}

// Themes get the global themes
func (tmpl *Template) Themes() []core.SelectOption {
	return tmpl.Template.Themes
}

// Assets get the assets
func (tmpl *Template) Assets() []string {
	return nil
}

// Glob the files
func (tmpl *Template) Glob(pattern string) ([]string, error) {
	path := filepath.Join(tmpl.Root, pattern)
	paths, err := tmpl.agent.fs.Glob(path)
	if err != nil {
		return nil, err
	}

	routes := []string{}
	for _, p := range paths {
		routes = append(routes, strings.TrimPrefix(p, tmpl.Root))
	}
	return routes, nil
}

// GlobRoutes the files
func (tmpl *Template) GlobRoutes(patterns []string, unique ...bool) ([]string, error) {
	routes := []string{}
	for _, pattern := range patterns {
		paths, err := tmpl.Glob(pattern)
		if err != nil {
			return nil, err
		}

		for _, path := range paths {
			if !tmpl.agent.fs.IsDir(filepath.Join(tmpl.Root, path)) {
				continue
			}
			routes = append(routes, path)
		}
	}

	if len(unique) > 0 && unique[0] {
		mapRoutes := map[string]bool{}
		for _, route := range routes {
			mapRoutes[route] = true
		}

		routes = []string{}
		for route := range mapRoutes {
			routes = append(routes, route)
		}
	}

	return routes, nil
}

// Reload the template
func (tmpl *Template) Reload() error {
	return nil
}

// PageTree gets the page tree
func (tmpl *Template) PageTree(route string) ([]*core.PageTreeNode, error) {
	return nil, nil
}

// MediaSearch search the asset
func (tmpl *Template) MediaSearch(query url.Values, page int, pageSize int) (core.MediaSearchResult, error) {
	return core.MediaSearchResult{Data: []core.Media{}, Page: page, PageSize: pageSize}, nil
}

// AssetUpload upload the asset (not supported)
func (tmpl *Template) AssetUpload(reader io.Reader, name string) (string, error) {
	return "", fmt.Errorf("AssetUpload is not supported for agent template")
}

// Block get the block (not supported)
func (tmpl *Template) Block(name string) (core.IBlock, error) {
	return nil, fmt.Errorf("Block is not supported for agent template")
}

// Blocks get the blocks (not supported)
func (tmpl *Template) Blocks() ([]core.IBlock, error) {
	return nil, nil
}

// BlockLayoutItems get block layout items
func (tmpl *Template) BlockLayoutItems() (*core.BlockLayoutItems, error) {
	return nil, nil
}

// BlockMedia get block media
func (tmpl *Template) BlockMedia(id string) (*core.Asset, error) {
	return nil, fmt.Errorf("BlockMedia is not supported for agent template")
}

// Component get the component (not supported)
func (tmpl *Template) Component(name string) (core.IComponent, error) {
	return nil, fmt.Errorf("Component is not supported for agent template")
}

// Components get the components (not supported)
func (tmpl *Template) Components() ([]core.IComponent, error) {
	return nil, nil
}

// SupportLocales get the support locales
func (tmpl *Template) SupportLocales() []string {
	locales := tmpl.Locales()
	result := make([]string, len(locales))
	for i, locale := range locales {
		result[i] = locale.Value
	}
	return result
}

// ExecBeforeBuildScripts execute the before build scripts
func (tmpl *Template) ExecBeforeBuildScripts() []core.TemplateScirptResult {
	return nil
}

// ExecAfterBuildScripts execute the after build scripts
func (tmpl *Template) ExecAfterBuildScripts() []core.TemplateScirptResult {
	return nil
}

// ExecBuildCompleteScripts execute the build complete scripts
func (tmpl *Template) ExecBuildCompleteScripts() []core.TemplateScirptResult {
	return nil
}

// Build build the template
func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) {
	warnings := []string{}

	// Execute before build scripts
	tmpl.ExecBeforeBuildScripts()

	root, err := tmpl.agent.DSL.PublicRoot(option.Data)
	if err != nil {
		log.Error("Build: Get the public root error: %s. use %s", err.Error(), tmpl.agent.DSL.Public.Root)
		root = tmpl.agent.DSL.Public.Root
	}

	if option.AssetRoot == "" {
		option.AssetRoot = filepath.Join(root, "assets")
	}
	option.PublicRoot = root

	// Sync the assets
	if err = tmpl.SyncAssets(option); err != nil {
		return warnings, err
	}

	// Get all pages
	pages, err := tmpl.Pages()
	if err != nil {
		return warnings, err
	}

	// Build global context
	globalCtx := core.NewGlobalBuildContext(tmpl)

	// Build each page
	tmpl.loaded = map[string]core.IPage{}
	for _, page := range pages {
		if err := page.Load(); err != nil {
			warnings = append(warnings, fmt.Sprintf("Failed to load page %s: %v", page.Get().Route, err))
			continue
		}

		pageWarnings, err := page.Build(globalCtx, option)
		if err != nil {
			warnings = append(warnings, fmt.Sprintf("Failed to build page %s: %v", page.Get().Route, err))
			continue
		}
		warnings = append(warnings, pageWarnings...)
		tmpl.loaded[page.Get().Route] = page
	}

	// Add sui lib to the global
	err = tmpl.UpdateJSSDK(option)
	if err != nil {
		return warnings, err
	}

	// Execute after build scripts
	tmpl.ExecAfterBuildScripts()

	return warnings, nil
}

// SyncAssets sync assets from template __assets to public
func (tmpl *Template) SyncAssets(option *core.BuildOption) error {
	// Get source abs path
	sourceRoot := filepath.Join(tmpl.agent.fs.Root(), tmpl.Root, "__assets")
	if exist, _ := os.Stat(sourceRoot); exist == nil {
		return nil
	}

	// Get target abs path
	root, err := tmpl.agent.DSL.PublicRoot(option.Data)
	if err != nil {
		log.Error("SyncAssets: Get the public root error: %s. use %s", err.Error(), tmpl.agent.DSL.Public.Root)
		root = tmpl.agent.DSL.Public.Root
	}

	targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
	if exist, _ := os.Stat(targetRoot); exist == nil {
		os.MkdirAll(targetRoot, os.ModePerm)
	}

	// Copy the assets
	return tmpl.copyDir(sourceRoot, targetRoot)
}

// copyDir copy directory recursively
func (tmpl *Template) copyDir(src string, dst string) error {
	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relPath, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}

		targetPath := filepath.Join(dst, relPath)

		if info.IsDir() {
			return os.MkdirAll(targetPath, os.ModePerm)
		}

		// Copy file
		data, err := os.ReadFile(path)
		if err != nil {
			return err
		}

		return os.WriteFile(targetPath, data, 0644)
	})
}

// SyncAssetFile sync asset file
func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error {
	sourceRoot := filepath.Join(tmpl.agent.fs.Root(), tmpl.Root, "__assets")
	if exist, _ := os.Stat(sourceRoot); exist == nil {
		return nil
	}

	root, err := tmpl.agent.DSL.PublicRoot(option.Data)
	if err != nil {
		log.Error("SyncAssetFile: Get the public root error: %s. use %s", err.Error(), tmpl.agent.DSL.Public.Root)
		root = tmpl.agent.DSL.Public.Root
	}

	targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
	sourceFile := filepath.Join(sourceRoot, file)
	targetFile := filepath.Join(targetRoot, file)

	// Create the target directory
	dir := filepath.Dir(targetFile)
	if exist, _ := os.Stat(dir); exist == nil {
		os.MkdirAll(dir, os.ModePerm)
	}

	// Copy file
	data, err := os.ReadFile(sourceFile)
	if err != nil {
		return err
	}

	return os.WriteFile(targetFile, data, 0644)
}

// UpdateJSSDK update the JS SDK (libsui.min.js)
func (tmpl *Template) UpdateJSSDK(option *core.BuildOption) error {
	root, err := tmpl.agent.DSL.PublicRoot(option.Data)
	if err != nil {
		log.Error("UpdateJSSDK: Get the public root error: %s. use %s", err.Error(), tmpl.agent.DSL.Public.Root)
		root = tmpl.agent.DSL.Public.Root
	}

	targetRoot := filepath.Join(application.App.Root(), "public", root, "assets")
	if exist, _ := os.Stat(targetRoot); exist == nil {
		os.MkdirAll(targetRoot, os.ModePerm)
	}

	// Get libsui source
	libsui, libsuiMap, err := core.LibSUI()
	if err != nil {
		return err
	}

	// Write libsui.min.js
	file := filepath.Join(targetRoot, "libsui.min.js")
	err = os.WriteFile(file, libsui, 0644)
	if err != nil {
		return err
	}

	// Write libsui.min.js.map
	mapFile := filepath.Join(targetRoot, "libsui.min.js.map")
	err = os.WriteFile(mapFile, libsuiMap, 0644)
	if err != nil {
		return err
	}

	return nil
}

// Trans translate the template
func (tmpl *Template) Trans(option *core.BuildOption) ([]string, error) {
	warnings := []string{}

	// Get all pages
	pages, err := tmpl.Pages()
	if err != nil {
		return warnings, err
	}

	// Build global context
	globalCtx := core.NewGlobalBuildContext(tmpl)

	// Translate each page
	for _, page := range pages {
		if err := page.Load(); err != nil {
			warnings = append(warnings, fmt.Sprintf("Failed to load page %s: %v", page.Get().Route, err))
			continue
		}

		pageWarnings, err := page.Trans(globalCtx, option)
		if err != nil {
			warnings = append(warnings, fmt.Sprintf("Failed to translate page %s: %v", page.Get().Route, err))
			continue
		}
		warnings = append(warnings, pageWarnings...)
	}

	return warnings, nil
}

// loadBuildScript load the build script
func (tmpl *Template) loadBuildScript() error {
	file, source, err := tmpl.backendScriptSource("__build.backend")
	if err != nil {
		return err
	}

	if file == "" {
		return nil
	}

	script, err := v8.MakeScript(source, file, 5*time.Second)
	if err != nil {
		return err
	}
	tmpl.BuildScript = &core.Script{Script: script}
	return nil
}

func (tmpl *Template) backendScriptSource(name string) (string, []byte, error) {
	path := filepath.Join(tmpl.Root, fmt.Sprintf("%s.ts", name))
	if !tmpl.agent.fs.IsFile(path) {
		path = filepath.Join(tmpl.Root, fmt.Sprintf("%s.js", name))
	}

	if !tmpl.agent.fs.IsFile(path) {
		return "", nil, nil
	}

	content, err := tmpl.agent.fs.ReadFile(path)
	if err != nil {
		return "", nil, err
	}
	return path, content, nil
}
