package service

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"

	"3588AdminBackend/internal/config"
	"3588AdminBackend/internal/storage"
)

var safeConfigName = regexp.MustCompile(`^[A-Za-z0-9_.-]+$`)

type ConfigPreviewService struct {
	cfg    *config.Config
	assets *storage.AssetsRepo
}

type ConfigSource struct {
	Name string `json:"name"`
	Path string `json:"path"`
}

type ConfigPreviewSources struct {
	Root      string         `json:"root"`
	Templates []ConfigSource `json:"templates"`
	Profiles  []ConfigSource `json:"profiles"`
	Overlays  []ConfigSource `json:"overlays"`
}

type ConfigPreviewRequest struct {
	Template      string
	Profile       string
	Overlays      []string
	ConfigID      string
	ConfigVersion string
	DeviceID      string
}

type ConfigPreviewResult struct {
	Request  ConfigPreviewRequest `json:"request"`
	Root     string               `json:"root"`
	Sha256   string               `json:"sha256"`
	Size     int                  `json:"size"`
	Metadata map[string]any       `json:"metadata"`
	JSON     string               `json:"json"`
}

type ConfigAssetImportResult struct {
	Root      string `json:"root"`
	Templates int    `json:"templates"`
	Profiles  int    `json:"profiles"`
	Overlays  int    `json:"overlays"`
}

func NewConfigPreviewService(cfg *config.Config, repo ...*storage.AssetsRepo) *ConfigPreviewService {
	var assets *storage.AssetsRepo
	if len(repo) > 0 {
		assets = repo[0]
	}
	return &ConfigPreviewService{cfg: cfg, assets: assets}
}

func (s *ConfigPreviewService) ListSources() (ConfigPreviewSources, error) {
	out := ConfigPreviewSources{}
	if s == nil || s.assets == nil {
		return out, nil
	}
	templates, err := s.assets.ListTemplates()
	if err != nil {
		return out, err
	}
	for _, item := range templates {
		out.Templates = append(out.Templates, ConfigSource{Name: item.Name, Path: repoAssetPath("templates", item.Name)})
	}
	profiles, err := s.assets.ListProfiles()
	if err != nil {
		return out, err
	}
	for _, item := range profiles {
		out.Profiles = append(out.Profiles, ConfigSource{Name: item.Name, Path: repoAssetPath("profiles", item.Name)})
	}
	overlays, err := s.assets.ListOverlays()
	if err != nil {
		return out, err
	}
	for _, item := range overlays {
		out.Overlays = append(out.Overlays, ConfigSource{Name: item.Name, Path: repoAssetPath("overlays", item.Name)})
	}
	sort.Slice(out.Templates, func(i, j int) bool { return out.Templates[i].Name < out.Templates[j].Name })
	sort.Slice(out.Profiles, func(i, j int) bool { return out.Profiles[i].Name < out.Profiles[j].Name })
	sort.Slice(out.Overlays, func(i, j int) bool { return out.Overlays[i].Name < out.Overlays[j].Name })
	if len(out.Templates) > 0 || len(out.Profiles) > 0 || len(out.Overlays) > 0 {
		out.Root = "SQLite"
	}
	return out, nil
}

func (s *ConfigPreviewService) Render(req ConfigPreviewRequest) (*ConfigPreviewResult, error) {
	if err := validateConfigName(req.Template); err != nil {
		return nil, fmt.Errorf("invalid template: %w", err)
	}
	if strings.TrimSpace(req.Profile) != "" {
		if err := validateConfigName(req.Profile); err != nil {
			return nil, fmt.Errorf("invalid profile: %w", err)
		}
	}
	for _, overlay := range req.Overlays {
		if err := validateConfigName(overlay); err != nil {
			return nil, fmt.Errorf("invalid overlay %q: %w", overlay, err)
		}
	}
	templateRaw, templatePath, err := s.readAssetJSON("templates", req.Template)
	if err != nil {
		return nil, err
	}
	profilePath := repoAssetPath("profiles", req.Profile)
	profileRaw := map[string]any{}
	if strings.TrimSpace(req.Profile) != "" {
		editor, err := s.GetProfileEditor(req.Profile)
		if err != nil {
			return nil, err
		}
		profileRaw, err = s.BuildProfileDocument(*editor)
		if err != nil {
			return nil, err
		}
	}
	selectedOverlays := req.Overlays
	if len(selectedOverlays) == 0 {
		selectedOverlays = profileOverlayNames(profileRaw)
	}
	overlays := make([]runtimeOverlayInput, 0, len(selectedOverlays))
	for _, overlay := range selectedOverlays {
		raw, path, err := s.readAssetJSON("overlays", overlay)
		if err != nil {
			return nil, err
		}
		overlays = append(overlays, runtimeOverlayInput{Name: overlay, Path: path, Raw: raw})
	}
	req.Overlays = append([]string(nil), selectedOverlays...)
	return s.renderFromAssets(req, templateRaw, templatePath, profileRaw, profilePath, overlays)
}

func (s *ConfigPreviewService) RenderProfileEditor(editor ConfigProfileEditor, req ConfigPreviewRequest) (*ConfigPreviewResult, error) {
	if err := validateConfigName(req.Template); err != nil {
		return nil, fmt.Errorf("invalid template: %w", err)
	}
	for _, overlay := range req.Overlays {
		if err := validateConfigName(overlay); err != nil {
			return nil, fmt.Errorf("invalid overlay %q: %w", overlay, err)
		}
	}
	doc, err := s.BuildProfileDocument(editor)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(req.Profile) == "" {
		req.Profile = strings.TrimSpace(editor.Name)
	}
	templateRaw, templatePath, err := s.readAssetJSON("templates", req.Template)
	if err != nil {
		return nil, err
	}
	selectedOverlays := req.Overlays
	if len(selectedOverlays) == 0 {
		selectedOverlays = profileOverlayNames(doc)
	}
	overlays := make([]runtimeOverlayInput, 0, len(selectedOverlays))
	for _, overlay := range selectedOverlays {
		raw, path, err := s.readAssetJSON("overlays", overlay)
		if err != nil {
			return nil, err
		}
		overlays = append(overlays, runtimeOverlayInput{Name: overlay, Path: path, Raw: raw})
	}
	req.Overlays = append([]string(nil), selectedOverlays...)
	return s.renderFromAssets(req, templateRaw, templatePath, doc, repoAssetPath("profiles", req.Profile), overlays)
}

func (s *ConfigPreviewService) BuildProfileEditorForDeviceAssignment(deviceID string) (*ConfigProfileEditor, *DeviceAssignmentAsset, error) {
	assignment, err := s.GetDeviceAssignment(deviceID)
	if err != nil {
		return nil, nil, err
	}
	editor, err := s.GetProfileEditor(assignment.ProfileName)
	if err != nil {
		return nil, nil, err
	}
	if len(assignment.RecognitionUnits) == 0 {
		editor.Instances = nil
		return editor, assignment, nil
	}
	selected := make(map[string]struct{}, len(assignment.RecognitionUnits))
	for _, ref := range assignment.RecognitionUnits {
		selected[ref] = struct{}{}
	}
	filtered := make([]ConfigProfileInstanceEditor, 0, len(assignment.RecognitionUnits))
	for _, inst := range editor.Instances {
		if _, ok := selected[recognitionUnitRef(editor.Name, inst.Name)]; ok {
			filtered = append(filtered, inst)
		}
	}
	editor.Instances = filtered
	return editor, assignment, nil
}

func (s *ConfigPreviewService) RenderDeviceAssignment(deviceID string) (*ConfigPreviewResult, error) {
	editor, assignment, err := s.BuildProfileEditorForDeviceAssignment(deviceID)
	if err != nil {
		return nil, err
	}
	if len(editor.Instances) == 0 {
		return nil, fmt.Errorf("设备 %s 还没有分配识别单元", deviceID)
	}
	templateName := firstProfileTemplate(editor.Instances)
	if strings.TrimSpace(templateName) == "" {
		templateName = editor.RawTemplateName()
	}
	return s.RenderProfileEditor(*editor, ConfigPreviewRequest{
		Template:      templateName,
		Profile:       assignment.ProfileName,
		ConfigID:      "assignment_" + deviceID,
		ConfigVersion: time.Now().Format("20060102.150405"),
		DeviceID:      deviceID,
	})
}

func profileOverlayNames(raw map[string]any) []string {
	items, _ := raw["overlays"].([]any)
	if len(items) == 0 {
		return nil
	}
	out := make([]string, 0, len(items))
	for _, item := range items {
		if v := stringAny(item); strings.TrimSpace(v) != "" {
			out = append(out, strings.TrimSpace(v))
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func (s *ConfigPreviewService) renderFromAssets(req ConfigPreviewRequest, templateRaw map[string]any, templatePath string, profileRaw map[string]any, profilePath string, overlays []runtimeOverlayInput) (*ConfigPreviewResult, error) {
	if err := validateConfigName(req.Template); err != nil {
		return nil, fmt.Errorf("invalid template: %w", err)
	}
	if strings.TrimSpace(req.Profile) != "" {
		if err := validateConfigName(req.Profile); err != nil {
			return nil, fmt.Errorf("invalid profile: %w", err)
		}
	}
	for _, overlay := range req.Overlays {
		if err := validateConfigName(overlay); err != nil {
			return nil, fmt.Errorf("invalid overlay %q: %w", overlay, err)
		}
	}
	if req.ConfigID == "" {
		req.ConfigID = "preview_" + time.Now().Format("20060102.150405")
	}
	if req.ConfigVersion == "" {
		req.ConfigVersion = time.Now().Format("20060102.150405")
	}
	resolvedProfileRaw, err := s.resolveSceneBindings(profileRaw)
	if err != nil {
		return nil, err
	}
	metadata := map[string]any{
		"config_id":      req.ConfigID,
		"config_version": req.ConfigVersion,
		"rendered_at":    time.Now().Format(time.RFC3339),
		"rendered_by":    "managerd",
	}
	if strings.TrimSpace(req.DeviceID) != "" {
		metadata["device_id"] = strings.TrimSpace(req.DeviceID)
	}
	doc, err := renderRuntimeConfig(templateRaw, templatePath, resolvedProfileRaw, profilePath, overlays, metadata)
	if err != nil {
		return nil, err
	}
	body, err := marshalConfigJSON(doc)
	if err != nil {
		return nil, err
	}
	renderedMetadata, _ := doc["metadata"].(map[string]any)
	sum := sha256.Sum256(body)
	return &ConfigPreviewResult{
		Request:  req,
		Root:     previewRenderRoot(templatePath, profilePath),
		Sha256:   hex.EncodeToString(sum[:]),
		Size:     len(body),
		Metadata: renderedMetadata,
		JSON:     string(body),
	}, nil
}

func (s *ConfigPreviewService) resolveSceneBindings(raw map[string]any) (map[string]any, error) {
	if raw == nil {
		return map[string]any{}, nil
	}
	body, err := json.Marshal(raw)
	if err != nil {
		return nil, err
	}
	var clone map[string]any
	if err := json.Unmarshal(body, &clone); err != nil {
		return nil, err
	}
	instances, _ := clone["instances"].([]any)
	for _, item := range instances {
		instanceMap, _ := item.(map[string]any)
		paramsMap, _ := instanceMap["params"].(map[string]any)
		if paramsMap == nil {
			paramsMap = map[string]any{}
			instanceMap["params"] = paramsMap
		}
		inputBindings, _ := instanceMap["input_bindings"].(map[string]any)
		if inputBindings == nil {
			inputBindings = map[string]any{}
			instanceMap["input_bindings"] = inputBindings
		}
		videoSourceRef := bindingField(inputBindings, "video_input_main", "video_source_ref")
		if videoSourceRef != "" {
			asset, err := s.GetVideoSource(videoSourceRef)
			if err != nil {
				return nil, fmt.Errorf("load video_source_ref %q: %w", videoSourceRef, err)
			}
			entry, _ := inputBindings["video_input_main"].(map[string]any)
			if entry == nil {
				entry = map[string]any{}
			}
			entry["video_source_ref"] = videoSourceRef
			entry["resolved"] = map[string]any{
				"url":          asset.Config.URL,
				"resolution":   asset.Config.Resolution,
				"frame_size":   asset.Config.FrameSize,
				"fps":          asset.Config.FPS,
				"video_format": asset.Config.VideoFormat,
			}
			inputBindings["video_input_main"] = entry
		}

		serviceBindings, _ := instanceMap["service_bindings"].(map[string]any)
		if serviceBindings == nil {
			serviceBindings = map[string]any{}
			instanceMap["service_bindings"] = serviceBindings
		}
		for _, binding := range []struct {
			slot     string
			expected string
		}{
			{slot: "object_storage_main", expected: "object_storage"},
			{slot: "token_service_main", expected: "token_service"},
			{slot: "alarm_service_main", expected: "alarm_service"},
		} {
			serviceRef := bindingField(serviceBindings, binding.slot, "service_ref")
			if serviceRef == "" {
				continue
			}
			asset, err := s.GetIntegrationService(serviceRef)
			if err != nil {
				return nil, fmt.Errorf("load service_ref %q: %w", serviceRef, err)
			}
			if strings.TrimSpace(asset.Type) != binding.expected {
				return nil, fmt.Errorf("service_ref %q has type %q, expected %q", serviceRef, asset.Type, binding.expected)
			}
			entry, _ := serviceBindings[binding.slot].(map[string]any)
			if entry == nil {
				entry = map[string]any{}
			}
			entry["service_ref"] = serviceRef
			entry["resolved"] = resolvedServiceBinding(asset)
			serviceBindings[binding.slot] = entry
		}
	}
	return clone, nil
}

func bindingField(bindings map[string]any, slot string, field string) string {
	entry, _ := bindings[slot].(map[string]any)
	return stringValue(entry[field])
}

func resolvedServiceBinding(asset *ConfigIntegrationServiceAsset) map[string]any {
	if asset == nil {
		return nil
	}
	switch asset.Type {
	case "object_storage":
		if asset.ObjectStorage == nil {
			return nil
		}
		return map[string]any{
			"endpoint":   asset.ObjectStorage.Endpoint,
			"bucket":     asset.ObjectStorage.Bucket,
			"access_key": asset.ObjectStorage.AccessKey,
			"secret_key": asset.ObjectStorage.SecretKey,
		}
	case "token_service":
		if asset.TokenService == nil {
			return nil
		}
		return map[string]any{
			"get_token_url": asset.TokenService.GetTokenURL,
			"username":      asset.TokenService.Username,
			"password":      asset.TokenService.Password,
			"tenant_code":   asset.TokenService.TenantCode,
		}
	case "alarm_service":
		if asset.AlarmService == nil {
			return nil
		}
		return map[string]any{
			"put_message_url": asset.AlarmService.PutMessageURL,
			"username":        asset.AlarmService.Username,
			"password":        asset.AlarmService.Password,
			"tenant_code":     asset.AlarmService.TenantCode,
		}
	default:
		return nil
	}
}

func previewRenderRoot(templatePath string, profilePath string) string {
	if strings.HasPrefix(templatePath, "sqlite:") || strings.HasPrefix(profilePath, "sqlite:") {
		return "SQLite"
	}
	if dir := filepath.Dir(templatePath); strings.TrimSpace(dir) != "" && dir != "." {
		return dir
	}
	return "managerd"
}

func writeResolvedConfigFile(pattern string, raw map[string]any) (string, error) {
	body, err := marshalConfigJSON(raw)
	if err != nil {
		return "", err
	}
	tempFile, err := os.CreateTemp("", pattern)
	if err != nil {
		return "", err
	}
	path := tempFile.Name()
	if _, err := tempFile.Write(body); err != nil {
		_ = tempFile.Close()
		_ = os.Remove(path)
		return "", err
	}
	_ = tempFile.Close()
	return path, nil
}

func setAnyString(m map[string]any, key string, value string) {
	if strings.TrimSpace(value) != "" {
		m[key] = strings.TrimSpace(value)
	}
}

func (s *ConfigPreviewService) mediaRepoRoot() string {
	if s.cfg == nil {
		return ""
	}
	if strings.TrimSpace(s.cfg.MediaRepoPath) == "" {
		return ""
	}
	return filepath.Clean(strings.TrimSpace(s.cfg.MediaRepoPath))
}

func listConfigSources(dir string) ([]ConfigSource, error) {
	files, err := os.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return []ConfigSource{}, nil
		}
		return nil, err
	}
	out := make([]ConfigSource, 0)
	for _, file := range files {
		if file.IsDir() || strings.ToLower(filepath.Ext(file.Name())) != ".json" {
			continue
		}
		name := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
		out = append(out, ConfigSource{Name: name, Path: filepath.Join(dir, file.Name())})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
	return out, nil
}

func validateConfigName(name string) error {
	if !safeConfigName.MatchString(strings.TrimSpace(name)) {
		return fmt.Errorf("must contain only letters, numbers, dot, underscore, or dash")
	}
	if strings.Contains(name, "..") {
		return fmt.Errorf("must not contain '..'")
	}
	return nil
}

func repoAssetPath(kind string, name string) string {
	return "sqlite:" + kind + "/" + strings.TrimSpace(name)
}

func (s *ConfigPreviewService) ImportAssetsFromMediaRepo() (*ConfigAssetImportResult, error) {
	if s == nil || s.assets == nil {
		return nil, fmt.Errorf("asset repository is not configured")
	}
	root := s.mediaRepoRoot()
	if root == "" {
		return nil, fmt.Errorf("legacy import source path is not configured")
	}
	result := &ConfigAssetImportResult{Root: root}
	for _, item := range []struct {
		kind string
		inc  *int
	}{
		{kind: "templates", inc: &result.Templates},
		{kind: "profiles", inc: &result.Profiles},
		{kind: "overlays", inc: &result.Overlays},
	} {
		sources, err := listConfigSources(filepath.Join(root, "configs", item.kind))
		if err != nil {
			return nil, err
		}
		for _, source := range sources {
			body, err := os.ReadFile(source.Path)
			if err != nil {
				return nil, err
			}
			var raw map[string]any
			if err := json.Unmarshal(body, &raw); err != nil {
				return nil, err
			}
			name := firstString(raw["name"], source.Name)
			description := stringValue(raw["description"])
			switch item.kind {
			case "templates":
				if raw == nil {
					raw = map[string]any{}
				}
				if isStandardTemplateName(name) && strings.TrimSpace(stringValue(raw["source"])) == "" {
					raw["source"] = "standard"
					body, err = marshalConfigJSON(raw)
					if err != nil {
						return nil, err
					}
				}
				if err := s.assets.SaveTemplate(name, description, string(body)); err != nil {
					return nil, err
				}
			case "profiles":
				if err := s.assets.SaveProfile(name, profileRawTemplateName(raw), stringValue(raw["business_name"]), description, string(body)); err != nil {
					return nil, err
				}
			case "overlays":
				if err := s.assets.SaveOverlay(name, description, string(body)); err != nil {
					return nil, err
				}
			}
			*item.inc = *item.inc + 1
		}
	}
	return result, nil
}

func (s *ConfigPreviewService) ExportAssetJSON(kind string, name string) ([]byte, string, error) {
	if err := validateConfigName(name); err != nil {
		return nil, "", err
	}
	if s == nil || s.assets == nil {
		return nil, "", fmt.Errorf("asset repository is not configured")
	}
	if body, ok, err := s.exportRepoAssetJSON(kind, name); ok || err != nil {
		return body, name + ".json", err
	}
	return nil, "", os.ErrNotExist
}

func (s *ConfigPreviewService) exportRepoAssetJSON(kind string, name string) ([]byte, bool, error) {
	var (
		record *storage.AssetRecord
		err    error
	)
	switch kind {
	case "templates":
		record, err = s.assets.GetTemplate(name)
	case "profiles":
		record, err = s.assets.GetProfile(name)
	case "overlays":
		record, err = s.assets.GetOverlay(name)
	default:
		return nil, true, fmt.Errorf("unsupported asset kind: %s", kind)
	}
	if err != nil {
		return nil, true, err
	}
	if record == nil {
		return nil, false, nil
	}
	return []byte(record.BodyJSON), true, nil
}

func profileRawTemplateName(raw map[string]any) string {
	instances, _ := raw["instances"].([]any)
	for _, item := range instances {
		instanceMap, _ := item.(map[string]any)
		if v := stringValue(instanceMap["template"]); v != "" {
			return v
		}
	}
	return stringValue(raw["primary_template_name"])
}
