//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package service

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"unicode"

	"ragflow/internal/dao"
	"ragflow/internal/entity"
	"ragflow/internal/utility"

	"gopkg.in/yaml.v3"
	"gorm.io/gorm"
)

// fillConfigDefaultLLM mirrors Python CompilationTemplateService.
// fill_config_default_llm: when the config has no explicit llm_id it lazily
// fills the tenant's default chat model id (read-side only; the DB row is left
// untouched).
func fillConfigDefaultLLM(ctx context.Context, tenantDAO *dao.TenantDAO, config entity.JSONMap, tenantID *string) entity.JSONMap {
	if len(config) == 0 || tenantID == nil || *tenantID == "" {
		return config
	}
	if _, ok := config["llm_id"]; ok {
		return config
	}
	tenant, err := tenantDAO.GetByID(ctx, dao.DB, *tenantID)
	if err != nil || tenant == nil || tenant.TenantLLMID == nil {
		return config
	}
	out := make(entity.JSONMap, len(config)+1)
	for k, v := range config {
		out[k] = v
	}
	out["llm_id"] = *tenant.TenantLLMID
	return out
}

// capitalizeTitle capitalizes the first letter of a lowercase section name for
// error messages (replaces deprecated strings.Title).
func capitalizeTitle(s string) string {
	if s == "" {
		return s
	}
	r := []rune(s)
	r[0] = unicode.ToUpper(r[0])
	return string(r)
}

// TemplateListItem is the read-side representation of a compilation template,
// mirroring Python CompilationTemplateService._to_saved_dict.
type TemplateListItem struct {
	ID          string         `json:"id"`
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Kind        string         `json:"kind"`
	Config      entity.JSONMap `json:"config"`
	CreateTime  string         `json:"create_time,omitempty"`
	UpdateTime  string         `json:"update_time,omitempty"`
}

// BuiltinTemplate is the palette representation of a built-in template,
// mirroring Python CompilationTemplateService._to_builtin_dict.
type BuiltinTemplate struct {
	ID          string         `json:"id"`
	Kind        string         `json:"kind"`
	DisplayName string         `json:"display_name"`
	Description string         `json:"description"`
	Config      entity.JSONMap `json:"config"`
}

// WikiPreset is a wiki page-structure preset loaded from YAML, mirroring
// Python load_wiki_presets_from_files.
type WikiPreset struct {
	ID          string `json:"id"`
	Topic       string `json:"topic"`
	Instruction string `json:"instruction"`
	PageExample string `json:"page_example"`
}

// CompilationTemplateService implements the read-side compilation template
// operations (builtins palette + wiki presets) used by the REST APIs.
type CompilationTemplateService struct {
	templateDAO *dao.CompilationTemplateDAO
	tenantDAO   *dao.TenantDAO
}

// NewCompilationTemplateService creates a CompilationTemplateService.
func NewCompilationTemplateService() *CompilationTemplateService {
	return &CompilationTemplateService{
		templateDAO: dao.NewCompilationTemplateDAO(),
		tenantDAO:   dao.NewTenantDAO(),
	}
}

// ListBuiltins returns the built-in template palette with the tenant's default
// LLM id lazily filled in, mirroring the Python list_builtins blueprint flow
// (list from DB; if empty, seed from files and retry; then fill default LLM).
func (s *CompilationTemplateService) ListBuiltins(ctx context.Context, tenantID string) ([]*BuiltinTemplate, error) {
	templates, err := s.templateDAO.ListBuiltins(ctx)
	if err != nil {
		return nil, err
	}
	if len(templates) == 0 {
		if err := s.seedBuiltins(ctx); err != nil {
			return nil, err
		}
		templates, err = s.templateDAO.ListBuiltins(ctx)
		if err != nil {
			return nil, err
		}
	}
	out := make([]*BuiltinTemplate, 0, len(templates))
	for _, t := range templates {
		config := fillConfigDefaultLLM(ctx, s.tenantDAO, t.Config, t.TenantID)
		out = append(out, &BuiltinTemplate{
			ID:          t.ID,
			Kind:        t.Kind,
			DisplayName: t.Name,
			Description: derefString(t.Description),
			Config:      config,
		})
	}
	return s.sortBuiltins(out), nil
}

// LoadWikiPresets loads the wiki page-structure presets from the
// init_data/compilation_templates/wiki/*.yaml files, filesystem-fresh per call.
func (s *CompilationTemplateService) LoadWikiPresets() ([]*WikiPreset, error) {
	wikiDir := filepath.Join(utility.GetProjectBaseDirectory(),
		"api", "db", "init_data", "compilation_templates", "wiki")
	entries, err := os.ReadDir(wikiDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []*WikiPreset{}, nil
		}
		return nil, err
	}
	var presets []*WikiPreset
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
			continue
		}
		path := filepath.Join(wikiDir, entry.Name())
		data, rerr := os.ReadFile(path)
		if rerr != nil {
			continue
		}
		var doc map[string]interface{}
		if yerr := yaml.Unmarshal(data, &doc); yerr != nil || doc == nil {
			continue
		}
		presets = append(presets, &WikiPreset{
			ID:          strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())),
			Topic:       strings.TrimSpace(yamlStr(doc["topic"])),
			Instruction: yamlStr(doc["instruction"]),
			PageExample: yamlStr(doc["page_example"]),
		})
	}
	return presets, nil
}

// ValidateTemplatePayload validates a single template payload, mirroring the
// Python compilation_template_validation module. It returns an error describing
// the first problem found.
func ValidateTemplatePayload(req map[string]interface{}, requireAll bool) error {
	if requireAll {
		for _, key := range []string{"name", "kind", "config"} {
			if _, ok := req[key]; !ok {
				return fmt.Errorf("missing required field: %s.", key)
			}
		}
	}
	if name, ok := req["name"]; ok {
		nameStr, ok2 := name.(string)
		if !ok2 || strings.TrimSpace(nameStr) == "" || len([]byte(nameStr)) > 128 {
			return errors.New("invalid template name.")
		}
	}
	if desc, ok := req["description"]; ok {
		if descStr, ok2 := desc.(string); !ok2 || len(descStr) > 1024 {
			return errors.New("invalid template description.")
		}
	}
	if kind, ok := req["kind"]; ok {
		if kindStr, ok2 := kind.(string); !ok2 || kindStr == "" {
			return errors.New("invalid template kind.")
		}
	}
	config, hasConfig := req["config"]
	if hasConfig {
		configMap, ok := config.(map[string]interface{})
		if !ok {
			return errors.New("invalid template config.")
		}
		if len(fmt.Sprint(configMap["global_rules"])) > 4096 {
			return errors.New("global compilation rules is too long.")
		}
		for _, section := range []string{"entity", "relation"} {
			sec, _ := configMap[section].(map[string]interface{})
			fields, _ := sec["fields"].([]interface{})
			seen := map[string]struct{}{}
			for _, f := range fields {
				fm, _ := f.(map[string]interface{})
				fieldType := strings.TrimSpace(yamlStr(fm["type"]))
				if fieldType == "" {
					return fmt.Errorf("%s type is required.", capitalizeTitle(section))
				}
				if _, dup := seen[fieldType]; dup {
					return fmt.Errorf("%s type can not be duplicated.", capitalizeTitle(section))
				}
				seen[fieldType] = struct{}{}
				if strings.TrimSpace(yamlStr(fm["description"])) == "" {
					return fmt.Errorf("%s field description is required.", capitalizeTitle(section))
				}
				if len(yamlStr(fm["description"])) > 1024 {
					return fmt.Errorf("%s field description is too long.", capitalizeTitle(section))
				}
				if len(yamlStr(fm["rule"])) > 1024 {
					return fmt.Errorf("%s field rule is too long.", capitalizeTitle(section))
				}
			}
		}
		if configMap["kind"] == "wiki" || req["kind"] == "wiki" {
			for _, group := range []string{"claim", "concept"} {
				sec, _ := configMap[group].(map[string]interface{})
				fields, _ := sec["fields"].([]interface{})
				for _, f := range fields {
					fm, _ := f.(map[string]interface{})
					switch group {
					case "claim":
						if strings.TrimSpace(yamlStr(fm["statement"])) == "" {
							return errors.New("claim statement is required.")
						}
						if strings.TrimSpace(yamlStr(fm["subject"])) == "" {
							return errors.New("claim subject is required.")
						}
						if len(yamlStr(fm["statement"])) > 1024 {
							return errors.New("claim statement is too long.")
						}
						if len(yamlStr(fm["subject"])) > 1024 {
							return errors.New("claim subject is too long.")
						}
					case "concept":
						if strings.TrimSpace(yamlStr(fm["term"])) == "" {
							return errors.New("concept term is required.")
						}
						if strings.TrimSpace(yamlStr(fm["definition_excerpt"])) == "" {
							return errors.New("concept definition excerpt is required.")
						}
						if len(yamlStr(fm["term"])) > 1024 {
							return errors.New("concept term is too long.")
						}
						if len(yamlStr(fm["definition_excerpt"])) > 1024 {
							return errors.New("concept definition excerpt is too long.")
						}
					}
				}
			}
		}
	}
	return nil
}

// sortBuiltins mirrors Python _sort_builtins: empty-kind entries first, then by
// display name.
func (s *CompilationTemplateService) sortBuiltins(templates []*BuiltinTemplate) []*BuiltinTemplate {
	sort.SliceStable(templates, func(i, j int) bool {
		emptyI := templates[i].Kind == "empty" || templates[i].ID == "empty"
		emptyJ := templates[j].Kind == "empty" || templates[j].ID == "empty"
		if emptyI != emptyJ {
			return emptyI
		}
		return strings.ToLower(templates[i].DisplayName) < strings.ToLower(templates[j].DisplayName)
	})
	return templates
}

// seedBuiltins loads the built-in templates from the init_data YAML files and
// upserts them (as a safety net when the DB has no built-ins yet), mirroring
// Python seed_builtins_from_files.
func (s *CompilationTemplateService) seedBuiltins(ctx context.Context) error {
	dir := filepath.Join(utility.GetProjectBaseDirectory(),
		"api", "db", "init_data", "compilation_templates")
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil // directory absent is acceptable; nothing to seed
	}
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
			continue
		}
		data, rerr := os.ReadFile(filepath.Join(dir, entry.Name()))
		if rerr != nil {
			continue
		}
		var doc struct {
			Kind        string                 `yaml:"kind"`
			DisplayName string                 `yaml:"display_name"`
			Description string                 `yaml:"description"`
			Config      map[string]interface{} `yaml:"config"`
		}
		if yerr := yaml.Unmarshal(data, &doc); yerr != nil {
			continue
		}
		if doc.Kind == "" || doc.DisplayName == "" || doc.Config == nil {
			continue
		}
		id := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
		desc := doc.Description
		valid := string(entity.StatusValid)
		tmpl := &entity.CompilationTemplate{
			ID:        id,
			Name:      doc.DisplayName,
			Kind:      doc.Kind,
			Config:    entity.JSONMap(doc.Config),
			IsBuiltin: true,
			Status:    &valid,
		}
		if doc.Description != "" {
			tmpl.Description = &desc
		}
		_ = s.upsertBuiltin(ctx, tmpl)
	}
	return nil
}

func (s *CompilationTemplateService) upsertBuiltin(ctx context.Context, t *entity.CompilationTemplate) error {
	existing, err := s.templateDAO.GetByID(ctx, t.ID)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return s.templateDAO.Save(ctx, dao.DB, t)
	}
	if err != nil {
		return err
	}
	m := map[string]interface{}{
		"name": t.Name, "kind": t.Kind, "config": t.Config,
		"description": t.Description, "is_builtin": true,
		"status": t.Status,
	}
	return s.templateDAO.UpdateFields(ctx, dao.DB, existing.ID, m)
}

func derefString(p *string) string {
	if p == nil {
		return ""
	}
	return *p
}

// yamlStr safely stringifies a YAML value, returning "" when the key is absent
// (instead of fmt.Sprint(nil) which yields "<nil>").
func yamlStr(v interface{}) string {
	if v == nil {
		return ""
	}
	return fmt.Sprint(v)
}
