// Package cmd defines all cp9 CLI commands and the ticat bootstrap.
package cmd

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/innerr/ticat/pkg/core/execute"
	"github.com/innerr/ticat/pkg/core/model"
	"github.com/innerr/ticat/pkg/core/parser"
	"github.com/innerr/ticat/pkg/mods/builtin"
)

const (
	selfName    = "cp9"
	rootDisplay = "<root>"
	builtinName = "builtin"
	builtinDisp = "<builtin>"
	pathSep     = "."
	abbrsSep    = "|"
	envOpSep    = ":"
	seqSep      = ":"
	envKVSep    = "="
	envPathSep  = "."
	listSep     = ","
	protoSep    = "\t"
	sysArgPfx   = "%"
	envDelAll   = "--"
	bracketL    = "{"
	bracketR    = "}"
	flowTplL    = "[["
	flowTplR    = "]]"
	flowTplMul  = "*"
	tagMark     = "@"
	trivialMark = "^"
	spaces      = "\t\n\r "
)

// Run initializes the ticat runtime and executes the CLI.
// Returns true if execution succeeded, false otherwise.
func Run() bool {
	if isRemovedPauseBuiltinAlias(os.Args[1:]) {
		_, _ = fmt.Fprintln(os.Stderr, "cp9 pause has been removed; use cp9 destroy for teardown and cleanup")
		return false
	}

	env := model.NewEnv().NewLayers(
		model.EnvLayerDefault,
		model.EnvLayerPersisted,
		model.EnvLayerSession,
	)

	defEnv := env.GetLayer(model.EnvLayerDefault)
	defEnv.Set("strs.self-name", selfName)
	defEnv.Set("strs.list-sep", listSep)
	defEnv.Set("strs.cmd-builtin-name", builtinName)
	defEnv.Set("strs.cmd-builtin-display-name", builtinDisp)
	defEnv.Set("strs.seq-sep", seqSep)
	defEnv.Set("strs.cmd-path-sep", pathSep)
	defEnv.Set("strs.env-path-sep", envPathSep)
	defEnv.Set("strs.env-op-sep", envOpSep)
	defEnv.Set("strs.env-kv-sep", envKVSep)
	defEnv.Set("strs.env-bracket-left", bracketL)
	defEnv.Set("strs.env-bracket-right", bracketR)
	defEnv.Set("strs.env-del-all-mark", envDelAll)
	defEnv.Set("strs.sys-arg-prefix", sysArgPfx)
	defEnv.Set("strs.proto-sep", protoSep)
	defEnv.Set("strs.flow-template-bracket-left", flowTplL)
	defEnv.Set("strs.flow-template-bracket-right", flowTplR)
	defEnv.Set("strs.flow-template-multiply-mark", flowTplMul)
	defEnv.Set("strs.tag-mark", tagMark)
	defEnv.Set("strs.trivial-mark", trivialMark)

	envKeysInfo := model.NewEnvKeysInfo()
	builtin.LoadDefaultEnv(env, envKeysInfo)

	// cp9-specific env defaults
	defEnv.SetBool("cp9.verbose", false)

	// Set up data/session paths under ~/.cp9/
	home, _ := os.UserHomeDir()
	dataDir := filepath.Join(home, ".cp9", "data")
	defEnv.Set("sys.paths.data", dataDir)
	defEnv.Set("sys.paths.sessions", filepath.Join(dataDir, "sessions"))
	defEnv.Set("sys.paths.hub", filepath.Join(dataDir, "hub"))
	defEnv.Set("sys.paths.cache", filepath.Join(dataDir, "cache"))
	defEnv.Set("sys.paths.flows", filepath.Join(dataDir, "flows"))
	defEnv.Set("sys.paths.env.snapshot", filepath.Join(dataDir, "env"))
	defEnv.Set("sys.paths.data.shared", filepath.Join(dataDir, "shared"))

	// Recover from panics by default
	env.SetBool("sys.panic.recover", true)

	tree := model.NewCmdTree(&model.CmdTreeStrs{
		SelfName: selfName, RootDisplayName: rootDisplay,
		BuiltinName: builtinName, BuiltinDisplayName: builtinDisp,
		PathSep: pathSep, PathAlterSeps: pathSep, AbbrsSep: abbrsSep, EnvOpSep: envOpSep, EnvValDelAllMark: envDelAll,
		EnvKeyValSep: envKVSep, EnvPathSep: envPathSep, ProtoSep: protoSep, ListSep: listSep,
		FlowTemplateBracketLeft: flowTplL, FlowTemplateBracketRight: flowTplR, FlowTemplateMultiplyMark: flowTplMul, TagMark: tagMark,
	})

	builtin.RegisterCmds(tree)
	registerCommands(tree)

	abbrs := model.NewEnvAbbrs(rootDisplay)
	builtin.LoadEnvAbbrs(abbrs)

	seqParser := parser.NewSequenceParser(
		seqSep,
		[]string{"http", "HTTP", "https", "HTTPS", "mysql", "MYSQL"},
		nil,
	)
	envParser := parser.NewEnvParser(
		parser.Brackets{Left: bracketL, Right: bracketR},
		spaces, envKVSep, envPathSep, sysArgPfx,
	)
	cmdParser := parser.NewCmdParser(
		envParser, pathSep, pathSep, spaces, rootDisplay, trivialMark, "/\\",
	)
	cliParser := parser.NewParser(seqParser, cmdParser)

	screen := model.NewStdScreen(os.Stdout, os.Stderr)
	cmdIO := model.NewCmdIO(os.Stdin, os.Stdout, os.Stderr)
	cc := model.NewCli(screen, tree, cliParser, abbrs, cmdIO, envKeysInfo)

	executor := execute.NewExecutor("env", "status", "<bootstrap>", "<entry>")
	cc.Executor = executor

	// Bootstrap: load runtime env, display platform, then user input
	bootstrap := `
		builtin.env.load.runtime:
		builtin.display.load.platform:
	`
	args := normalizeSubcommandPathAliases(normalizeCLIArgAliases(os.Args[1:]), tree)
	return executor.Run(cc, env, bootstrap, args...)
}

func normalizeCLIArgAliases(args []string) []string {
	return normalizeImageArgs(normalizeLongFlagArgs(normalizeBooleanArgAliases(args)))
}

func normalizeImageArgs(args []string) []string {
	for i, arg := range args {
		if image, ok := strings.CutPrefix(arg, "image="); ok {
			// Escape TiCat's ':' sequence separator inside OCI image references.
			image = strings.ReplaceAll(image, `\:`, ":")
			args[i] = "image=" + strings.ReplaceAll(image, ":", `\:`)
		}
	}
	return args
}

func normalizeLongFlagArgs(args []string) []string {
	normalized := make([]string, 0, len(args))
	for i := 0; i < len(args); i++ {
		arg := args[i]
		if arg == "--" {
			normalized = append(normalized, args[i:]...)
			break
		}
		if !strings.HasPrefix(arg, "--") || strings.Contains(arg, "=") {
			if after, ok := strings.CutPrefix(arg, "--"); ok && strings.Contains(after, "=") {
				normalized = append(normalized, after)
			} else {
				normalized = append(normalized, arg)
			}
			continue
		}
		name := strings.TrimPrefix(arg, "--")
		if name == "" || i+1 >= len(args) || strings.HasPrefix(args[i+1], "--") {
			normalized = append(normalized, arg)
			continue
		}
		normalized = append(normalized, name+"="+args[i+1])
		i++
	}
	return normalized
}

func normalizeSubcommandPathAliases(args []string, tree *model.CmdTree) []string {
	if len(args) < 2 || tree == nil {
		return args
	}
	rootSub := tree.GetSub(args[0])
	if rootSub == nil || rootSub.GetSub(args[1]) == nil {
		return args
	}
	normalized := make([]string, 0, len(args)-1)
	normalized = append(normalized, args[0]+pathSep+args[1])
	normalized = append(normalized, args[2:]...)
	return normalized
}

func normalizeBooleanArgAliases(args []string) []string {
	if len(args) == 0 {
		return args
	}
	commandIndex := 0
	for commandIndex < len(args) &&
		strings.HasPrefix(args[commandIndex], bracketL) &&
		strings.HasSuffix(args[commandIndex], bracketR) {
		commandIndex++
	}
	if commandIndex >= len(args) {
		return args
	}
	var aliases map[string]string
	switch args[commandIndex] {
	case "up", "deploy":
		aliases = map[string]string{
			"dry-run":   "dry-run=true",
			"--dry-run": "dry-run=true",
		}
	case "status", "st":
		aliases = map[string]string{
			"watch":   "watch=true",
			"--watch": "watch=true",
			"-w":      "watch=true",
		}
	default:
		return args
	}
	normalized := append([]string(nil), args...)
	for i := commandIndex + 1; i < len(normalized); i++ {
		if replacement, ok := aliases[normalized[i]]; ok {
			normalized[i] = replacement
		}
	}
	return normalized
}

func isRemovedPauseBuiltinAlias(args []string) bool {
	return len(args) > 0 && args[0] == "pause"
}
