package config

import (
	"fmt"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/creasty/defaults"
	"github.com/go-playground/validator/v10"

	sdkconfig "github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"
)

var defaultDeniedDirectories = []string{
	".git", ".hg", ".svn", "node_modules", ".venv", "venv", "vendor", "dist", "build", ".next", ".cache", "__pycache__",
}

var defaultIgnoredProjectDirectories = []string{"lost+found"}

type RootConfig struct {
	Log       sdkconfig.Loggers `yaml:"log"`
	RPCServer RPCServerConfig   `yaml:"rpcServer"`
	Services  ServicesConfig    `yaml:"services"`
}

type RPCServerConfig struct {
	Address string `yaml:"address" default:":9091" validate:"required"`
}

type ServicesConfig struct {
	Project     ProjectServiceConfig     `yaml:"project"`
	SecretStore SecretStoreServiceConfig `yaml:"secretStore"`
	APIProxy    APIProxyServiceConfig    `yaml:"apiProxy"`
}

type ProjectServiceConfig struct {
	ProjectsRootDir           string   `yaml:"projectsRootDir" default:"/data/projects" validate:"required"`
	CodexHomeDir              string   `yaml:"codexHomeDir" default:"/home/node/.codex" validate:"required"`
	GitBinary                 string   `yaml:"gitBinary" default:"git" validate:"required"`
	MaxEditableFileSize       int64    `yaml:"maxEditableFileSize" default:"1048576" validate:"gt=0"`
	DefaultPageSize           int      `yaml:"defaultPageSize" default:"100" validate:"gt=0"`
	MaxPageSize               int      `yaml:"maxPageSize" default:"500" validate:"gt=0"`
	MaxPathLength             int      `yaml:"maxPathLength" default:"4096" validate:"gt=0"`
	MaxPathDepth              int      `yaml:"maxPathDepth" default:"64" validate:"gt=0"`
	DeniedDirectories         []string `yaml:"deniedDirectories" validate:"dive,required"`
	IgnoredProjectDirectories []string `yaml:"ignoredProjectDirectories" validate:"dive,required"`
}

func Default() RootConfig {
	conf := RootConfig{}
	defaults.MustSet(&conf)
	conf.Services.Project.DeniedDirectories = slices.Clone(defaultDeniedDirectories)
	conf.Services.Project.IgnoredProjectDirectories = slices.Clone(defaultIgnoredProjectDirectories)
	return conf
}

func (c *RootConfig) Normalize() {
	defaults.MustSet(c)
	for name, runtime := range c.Services.SecretStore.Runtimes {
		if runtime.Source.Type == SecretSourceLarkTenantToken && runtime.Refresh.RefreshBeforeExpiry == 0 {
			runtime.Refresh.RefreshBeforeExpiry = 5 * time.Minute
			c.Services.SecretStore.Runtimes[name] = runtime
		}
	}
	if c.Services.Project.DeniedDirectories == nil {
		c.Services.Project.DeniedDirectories = slices.Clone(defaultDeniedDirectories)
	}
	if c.Services.Project.IgnoredProjectDirectories == nil {
		c.Services.Project.IgnoredProjectDirectories = slices.Clone(defaultIgnoredProjectDirectories)
	}
	denied := make(map[string]struct{}, len(c.Services.Project.DeniedDirectories)+1)
	for _, name := range c.Services.Project.DeniedDirectories {
		denied[name] = struct{}{}
	}
	denied[".git"] = struct{}{}
	c.Services.Project.DeniedDirectories = c.Services.Project.DeniedDirectories[:0]
	for name := range denied {
		c.Services.Project.DeniedDirectories = append(c.Services.Project.DeniedDirectories, name)
	}
	slices.Sort(c.Services.Project.DeniedDirectories)
	c.Services.Project.IgnoredProjectDirectories = uniqueSorted(c.Services.Project.IgnoredProjectDirectories)
}

func (c *RootConfig) Validate() error {
	validate := validator.New(validator.WithRequiredStructEnabled())
	if err := validate.Struct(c); err != nil {
		return err
	}
	project := c.Services.Project
	if !filepath.IsAbs(project.ProjectsRootDir) || !filepath.IsAbs(project.CodexHomeDir) {
		return fmt.Errorf("project root directories must be absolute")
	}
	if filepath.Clean(project.ProjectsRootDir) == filepath.Clean(project.CodexHomeDir) {
		return fmt.Errorf("projectsRootDir and codexHomeDir must be different")
	}
	if project.DefaultPageSize > project.MaxPageSize {
		return fmt.Errorf("defaultPageSize must not exceed maxPageSize")
	}
	for _, name := range project.DeniedDirectories {
		if err := validateDirectoryName("denied", name); err != nil {
			return err
		}
	}
	for _, name := range project.IgnoredProjectDirectories {
		if err := validateDirectoryName("ignored project", name); err != nil {
			return err
		}
	}
	if err := c.Services.SecretStore.Validate(); err != nil {
		return fmt.Errorf("secretStore: %w", err)
	}
	if err := c.Services.APIProxy.Validate(c.Services.SecretStore); err != nil {
		return fmt.Errorf("apiProxy: %w", err)
	}
	return nil
}

func uniqueSorted(values []string) []string {
	result := slices.Clone(values)
	slices.Sort(result)
	return slices.Compact(result)
}

func validateDirectoryName(kind, name string) error {
	if name == "." || name == ".." || strings.ContainsAny(name, "/\x00") {
		return fmt.Errorf("%s directory %q must be one path segment", kind, name)
	}
	return nil
}
