package service

import (
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "sort"
    "strings"
)

// FeatureDef describes a detection feature that users can toggle on/off.
type FeatureDef struct {
    Key       string   // unique key, e.g. "face", "shoe"
    Label     string   // human-readable label
    Templates []string // template names supporting this feature, ordered by coverage (broadest first)
}

// FeatureRegistry centralises which templates cover which detection features.
type FeatureRegistry struct {
    features map[string]*FeatureDef
    // nodeTypeRequirements maps feature key → required media-server node types.
    // If a device lacks any required node type, the feature is unavailable.
    nodeTypeRequirements map[string][]string
}

// NewFeatureRegistry creates a registry with the built-in feature definitions.
func NewFeatureRegistry() *FeatureRegistry {
    defs := []FeatureDef{
        {
            Key:       "face",
            Label:     "人脸识别",
            Templates: []string{"std_workshop_face_recognition_shoe_alarm", "std_face_recognition_stream"},
        },
        {
            Key:       "shoe",
            Label:     "劳保鞋检测",
            Templates: []string{"std_workshop_face_recognition_shoe_alarm", "std_workshoe_detection_stream"},
        },
        {
            Key:       "helmet",
            Label:     "安全帽检测",
            Templates: []string{}, // 模型就绪后再补充
        },
        {
            Key:       "smoking",
            Label:     "抽烟检测",
            Templates: []string{}, // 模型就绪后再补充
        },
        {
            Key:       "intrusion",
            Label:     "区域入侵",
            Templates: []string{}, // 模板就绪后再补充
        },
    }
    r := &FeatureRegistry{
        features:             make(map[string]*FeatureDef, len(defs)),
        nodeTypeRequirements: map[string][]string{
            "face":      {"ai_face_recog"},                                     // face recognition plugin
            "shoe":      {"ai_shoe_det"},                                        // shoe detection plugin
            "helmet":    {"ai_yolo"},                                             // YOLO for helmet (model pending)
            "smoking":   {"action_recog"},                                        // action recognition for smoking
            "intrusion": {"region_event"},                                        // region-based intrusion
        },
    }
    for i := range defs {
        r.features[defs[i].Key] = &defs[i]
    }
    return r
}

// FeatureKeys returns all registered feature keys in a stable order.
func (r *FeatureRegistry) FeatureKeys() []string {
    keys := make([]string, 0, len(r.features))
    for k := range r.features {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    return keys
}

// Feature returns the definition for a feature key, or nil.
// AvailableFeatures returns the subset of registered features that are supported
// by the given device node types (from /v1/graph-node-types).
// If nodeTypes is nil/empty, all features are considered available (fallback for
// old agents that don't expose node types).
func (r *FeatureRegistry) AvailableFeatures(nodeTypes []string) []FeatureDef {
    all := r.AllFeatures()
    if len(nodeTypes) == 0 {
        return all // no info → assume all available
    }
    typeSet := make(map[string]bool, len(nodeTypes))
    for _, t := range nodeTypes {
        typeSet[t] = true
    }
    out := make([]FeatureDef, 0, len(all))
    for _, f := range all {
        if r.featureAvailable(f.Key, typeSet) {
            out = append(out, f)
        }
    }
    return out
}

func (r *FeatureRegistry) featureAvailable(key string, nodeTypes map[string]bool) bool {
    required, ok := r.nodeTypeRequirements[key]
    if !ok || len(required) == 0 {
        return true // no specific plugin requirement → always available
    }
    for _, t := range required {
        if !nodeTypes[t] {
            return false
        }
    }
    return true
}

func (r *FeatureRegistry) Feature(key string) *FeatureDef {
    return r.features[key]
}

// AllFeatures returns a copy of all feature definitions sorted by key.
func (r *FeatureRegistry) AllFeatures() []FeatureDef {
    out := make([]FeatureDef, 0, len(r.features))
    for _, key := range r.FeatureKeys() {
        if f := r.features[key]; f != nil {
            out = append(out, *f)
        }
    }
    return out
}

// ResolveResult describes the outcome of template resolution.
type ResolveResult struct {
    TemplateName string `json:"template_name"` // selected template name; may be a composed name when Composed is true
    Composed     bool   `json:"composed"`      // true when no single template covers all features and composition is needed

    // internal fields for the template composer (unexported)
    composePrimary  string   `json:"-"`
    composeFeatures []string `json:"-"`
}

// ResolveTemplate picks the best template covering all requested features.
//
// Strategy:
//  1. Single-template: find the intersection of all features' template lists.
//     Return the first (highest-coverage) match.
//  2. Composition needed: every feature has at least one template but no single
//     template covers all. This case will be handled by the template composer
//     later — for now we return the broadest template and set Composed=true.
//  3. Error: one or more features have no templates at all.
func (r *FeatureRegistry) ResolveTemplate(featureKeys []string) (*ResolveResult, error) {
    if len(featureKeys) == 0 {
        return nil, fmt.Errorf("未选择任何检测功能")
    }

    // Deduplicate and sort
    seen := map[string]struct{}{}
    cleaned := make([]string, 0, len(featureKeys))
    for _, key := range featureKeys {
        key = strings.TrimSpace(key)
        if key == "" {
            continue
        }
        if _, ok := seen[key]; ok {
            continue
        }
        seen[key] = struct{}{}
        if _, ok := r.features[key]; !ok {
            return nil, fmt.Errorf("未知的检测功能: %q", key)
        }
        cleaned = append(cleaned, key)
    }
    if len(cleaned) == 0 {
        return nil, fmt.Errorf("未选择任何检测功能")
    }
    sort.Strings(cleaned)

    // Collect template lists for each feature.
    lists := make([][]string, 0, len(cleaned))
    for _, key := range cleaned {
        def := r.features[key]
        if len(def.Templates) == 0 {
            return nil, fmt.Errorf("%s（%s）的模板尚未就绪", def.Label, def.Key)
        }
        lists = append(lists, def.Templates)
    }

    // Find intersection.
    intersection := intersectTemplateLists(lists)
    if len(intersection) > 0 {
        // Pick the most specific template: the one covering the fewest features
        // (i.e. listed by the fewest FeatureDefs), not the broadest.
        // This ensures selecting only "face" doesn't pull in a face+shoe template.
        best := intersection[0]
        bestScore := r.templateFeatureCount(best)
        for _, tpl := range intersection[1:] {
            if score := r.templateFeatureCount(tpl); score < bestScore {
                best = tpl
                bestScore = score
            }
        }
        return &ResolveResult{
            TemplateName: best,
            Composed:     false,
        }, nil
    }

    // No single template covers all features → composition needed.
    // For now, pick the template that covers the most features.
    primary := pickBroadestTemplate(lists)
    composedName := "auto_" + strings.Join(cleaned, "_")

    return &ResolveResult{
        TemplateName:    composedName,
        Composed:        true,
        composePrimary:  primary,
        composeFeatures: append([]string(nil), cleaned...),
    }, nil
}

// intersectTemplateLists returns templates that appear in every list, preserving
// the order of the first list.
func intersectTemplateLists(lists [][]string) []string {
    if len(lists) == 0 {
        return nil
    }
    // Count occurrences across all lists.
    counts := map[string]int{}
    for _, list := range lists {
        lseen := map[string]struct{}{} // dedupe within one list
        for _, t := range list {
            if _, ok := lseen[t]; ok {
                continue
            }
            lseen[t] = struct{}{}
            counts[t]++
        }
    }
    total := len(lists)
    // Return in the order of the first list (which is sorted by coverage).
    result := make([]string, 0)
    seen := map[string]struct{}{}
    for _, t := range lists[0] {
        if counts[t] == total {
            if _, ok := seen[t]; !ok {
                seen[t] = struct{}{}
                result = append(result, t)
            }
        }
    }
    return result
}

// pickBroadestTemplate returns the template that appears earliest in the
// first list (assumed to be sorted by coverage breadth).
func pickBroadestTemplate(lists [][]string) string {
    if len(lists) == 0 || len(lists[0]) == 0 {
        return ""
    }
    return lists[0][0]
}

// templateFeatureCount returns how many features list this template.
// Lower = more specific template.
func (r *FeatureRegistry) templateFeatureCount(templateName string) int {
    count := 0
    for _, def := range r.features {
        for _, t := range def.Templates {
            if t == templateName {
                count++
                break
            }
        }
    }
    return count
}

func (r *FeatureRegistry) ValidateFeatureKeys(keys []string) error {
    seen := map[string]struct{}{}
    for _, key := range keys {
        key = strings.TrimSpace(key)
        if key == "" {
            continue
        }
        if _, ok := seen[key]; ok {
            continue
        }
        seen[key] = struct{}{}
        if _, ok := r.features[key]; !ok {
            return fmt.Errorf("未知的检测功能: %q", key)
        }
    }
    if len(seen) == 0 {
        return fmt.Errorf("未选择任何检测功能")
    }
    return nil
}

// ────────────────────────────────────────────────────────────
// AutoConfigService — one-click configuration pipeline
// ────────────────────────────────────────────────────────────

// AutoConfigRequest is a single device's configuration intent from the console.
type AutoConfigRequest struct {
    DeviceID     string   `json:"device_id"`
    Features     []string `json:"features"`      // feature keys: "face", "shoe", ...
    SourceNames  []string `json:"source_names"`   // assigned video source names
}

// AutoConfigResult reports what happened for a single device.
type AutoConfigResult struct {
    DeviceID      string `json:"device_id"`
    TemplateName  string `json:"template_name"`
    Composed      bool   `json:"composed"`
    UnitsCreated  int    `json:"units_created"`
    ConfigSHA256  string `json:"config_sha256"`
    TaskID        string `json:"task_id,omitempty"`
    Error         string `json:"error,omitempty"`
}

// AutoConfigService orchestrates the full pipeline from user intent to deployed config.
type AutoConfigService struct {
    preview  *ConfigPreviewService
    tasks    *TaskService
    registry *FeatureRegistry
}

// NewAutoConfigService creates a new AutoConfigService.
func NewAutoConfigService(preview *ConfigPreviewService, tasks *TaskService, registry *FeatureRegistry) *AutoConfigService {
    return &AutoConfigService{
        preview:  preview,
        tasks:    tasks,
        registry: registry,
    }
}

// BuildPipeline executes the full auto-config pipeline for a single device:
//  1. Resolve template from feature selection
//  2. If composed, generate the composed template
//  3. Create/update the scene template (profile)
//  4. Create/update recognition units for each video source
//  5. Create/update the device assignment
//  6. Render the final config
//  7. Create a deploy task
//
// If deploy is false, steps 6-7 are skipped (dry-run mode).
func (s *AutoConfigService) BuildPipeline(req AutoConfigRequest, deploy bool) (*AutoConfigResult, error) {
    result := &AutoConfigResult{DeviceID: req.DeviceID}

    // ---- 1. Resolve template ----
    cleanSources := cleanSourceNames(req.SourceNames)
    if len(cleanSources) == 0 {
        result.Error = "未分配任何视频源"
        return result, fmt.Errorf(result.Error)
    }

    resolveResult, err := s.registry.ResolveTemplate(req.Features)
    if err != nil {
        result.Error = err.Error()
        return result, err
    }

    templateName := resolveResult.TemplateName
    result.Composed = resolveResult.Composed

    // ---- 2. Compose template if needed ----
    if resolveResult.Composed {
        composed, err := s.composeFromFeatureTemplates(req.Features, resolveResult.composePrimary, resolveResult.composeFeatures)
        if err != nil {
            result.Error = fmt.Sprintf("模板组合失败: %v", err)
            return result, fmt.Errorf(result.Error)
        }
        body, err := marshalConfigJSON(composed)
        if err != nil {
            result.Error = fmt.Sprintf("序列化组合模板失败: %v", err)
            return result, fmt.Errorf(result.Error)
        }
        if err := s.preview.SaveTemplateAsset(templateName, "自动组合: "+strings.Join(req.Features, "+"), string(body)); err != nil {
            result.Error = fmt.Sprintf("保存组合模板失败: %v", err)
            return result, fmt.Errorf(result.Error)
        }
    }
    result.TemplateName = templateName

    // ---- 3. Ensure profile (scene template) ----
    profileName := "auto_" + req.DeviceID
    editor := ConfigProfileEditor{
        Name:                profileName,
        PrimaryTemplateName: templateName,
        BusinessName:        req.DeviceID + " 自动配置",
        Description:         "管控台一键生成",
    }

    // ---- 4. Create recognition units for each video source ----
    for _, srcName := range cleanSources {
        unitName := deriveSafeInstanceName(srcName)
        inst := ConfigProfileInstanceEditor{
            Name:           unitName,
            Template:       templateName,
            VideoSourceRef: srcName,
            DisplayName:    srcName,
            InputBindings: map[string]InputBindingEditor{
                "video_input_main": {VideoSourceRef: srcName},
            },
            OutputBindings: map[string]OutputBindingEditor{
                "stream_output_main": {
                    PublishHLSPath:  "./web/hls/" + unitName + "/index.m3u8",
                    PublishRTSPPort: "8555",
                    PublishRTSPPath: "/live/" + unitName,
                    ChannelNo:       unitName,
                },
            },
        }
        editor.Instances = append(editor.Instances, inst)
    }

    if err := s.preview.SaveProfileEditor(editor); err != nil {
        result.Error = fmt.Sprintf("保存场景模板失败: %v", err)
        return result, fmt.Errorf(result.Error)
    }

    // ---- 4b. Save recognition units via the dedicated API ----
    for _, inst := range editor.Instances {
        unit := RecognitionUnitAsset{
            SceneTemplateName: profileName,
            Name:              inst.Name,
            DisplayName:       inst.DisplayName,
            VideoSourceRef:    inst.VideoSourceRef,
            OutputChannel:     inst.ChannelNo,
            RTSPPort:          inst.PublishRTSPPort,
        }
        ref := recognitionUnitRef(profileName, inst.Name)
        if err := s.preview.SaveRecognitionUnit(unit, ref); err != nil {
            result.Error = fmt.Sprintf("保存识别单元 %s 失败: %v", inst.Name, err)
            return result, fmt.Errorf(result.Error)
        }
        result.UnitsCreated++
    }

    // ---- 5. Device assignment ----
    unitRefs := make([]string, 0, len(cleanSources))
    for _, inst := range editor.Instances {
        unitRefs = append(unitRefs, recognitionUnitRef(profileName, inst.Name))
    }
    assignment := DeviceAssignmentAsset{
        DeviceID:         req.DeviceID,
        ProfileName:      profileName,
        Description:      "管控台自动分配",
        RecognitionUnits: unitRefs,
    }
    if err := s.preview.SaveDeviceAssignment(assignment); err != nil {
        result.Error = fmt.Sprintf("保存设备分配失败: %v", err)
        return result, fmt.Errorf(result.Error)
    }

    if !deploy {
        preview, err := s.preview.RenderDeviceAssignment(req.DeviceID)
        if err != nil {
            result.Error = fmt.Sprintf("配置渲染失败: %v", err)
            return result, fmt.Errorf(result.Error)
        }
        result.ConfigSHA256 = preview.Sha256
        return result, nil
    }

    // ---- 6 & 7. Render and deploy ----
    preview, err := s.preview.RenderDeviceAssignment(req.DeviceID)
    if err != nil {
        result.Error = fmt.Sprintf("配置渲染失败: %v", err)
        return result, fmt.Errorf(result.Error)
    }
    result.ConfigSHA256 = preview.Sha256

    var configDoc any
    if err := json.Unmarshal([]byte(preview.JSON), &configDoc); err != nil {
        result.Error = fmt.Sprintf("配置 JSON 无效: %v", err)
        return result, fmt.Errorf(result.Error)
    }

    task, err := s.tasks.CreateTask("config_apply", []string{req.DeviceID}, map[string]any{"config": configDoc})
    if err != nil {
        result.Error = fmt.Sprintf("创建下发任务失败: %v", err)
        return result, fmt.Errorf(result.Error)
    }
    result.TaskID = task.ID
    return result, nil
}

// BuildPipelineBatch executes the pipeline for multiple devices.
func (s *AutoConfigService) BuildPipelineBatch(requests []AutoConfigRequest, deploy bool) ([]AutoConfigResult, error) {
    results := make([]AutoConfigResult, 0, len(requests))
    var firstErr error
    for _, req := range requests {
        result, err := s.BuildPipeline(req, deploy)
        if result != nil {
            results = append(results, *result)
        }
        if err != nil && firstErr == nil {
            firstErr = err
        }
    }
    return results, firstErr
}

// composeFromFeatureTemplates loads templates for each feature and composes them.
func (s *AutoConfigService) composeFromFeatureTemplates(featureKeys []string, primaryName string, allFeatures []string) (map[string]any, error) {
    primaryRaw, _, err := s.preview.readAssetJSON("templates", primaryName)
    if err != nil {
        return nil, fmt.Errorf("load primary template %q: %w", primaryName, err)
    }
    templates := []map[string]any{primaryRaw}
    for _, key := range featureKeys {
        if def := s.registry.Feature(key); def != nil {
            for _, tplName := range def.Templates {
                if tplName == primaryName {
                    continue
                }
                raw, _, err := s.preview.readAssetJSON("templates", tplName)
                if err != nil {
                    continue
                }
                templates = append(templates, raw)
                break
            }
        }
    }
    if len(templates) <= 1 {
        return primaryRaw, nil
    }
    return ComposeTemplates(templates)
}

// ---- Device feature persistence ----

// GetDeviceFeatures returns the persisted feature keys for a device.
func (s *ConfigPreviewService) GetDeviceFeatures(deviceID string) ([]string, error) {
    if s == nil || s.assets == nil {
        return nil, nil
    }
    rec, err := s.assets.GetDeviceFeatures(deviceID)
    if err != nil || rec == nil {
        return nil, err
    }
    var features []string
    if err := json.Unmarshal([]byte(rec.FeaturesJSON), &features); err != nil {
        return nil, nil
    }
    return features, nil
}

// SaveDeviceFeatures persists the feature keys for a device.
func (s *ConfigPreviewService) SaveDeviceFeatures(deviceID string, features []string) error {
    if s == nil || s.assets == nil {
        return fmt.Errorf("asset repository is not configured")
    }
    if len(features) == 0 {
        features = []string{}
    }
    body, err := json.Marshal(features)
    if err != nil {
        return err
    }
    return s.assets.SaveDeviceFeatures(deviceID, string(body))
}

func cleanSourceNames(names []string) []string {
    seen := map[string]struct{}{}
    out := make([]string, 0, len(names))
    for _, n := range names {
        n = strings.TrimSpace(n)
        if n == "" {
            continue
        }
        if _, ok := seen[n]; ok {
            continue
        }
        seen[n] = struct{}{}
        out = append(out, n)
    }
    sort.Strings(out)
    return out
}

// deriveSafeInstanceName converts a user-facing video source name into a valid
// recognition unit instance name ([A-Za-z0-9_.-]+). If the source name is already
// valid, it's used as-is. Otherwise, a sanitised version is generated.
func deriveSafeInstanceName(srcName string) string {
    // If already valid per validateConfigName rules, use as-is.
    if safeConfigName.MatchString(srcName) {
        return srcName
    }
    // Build a safe name from the source: keep only ASCII letters, digits, underscores.
    var b strings.Builder
    for _, r := range srcName {
        if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == '.' {
            b.WriteRune(r)
        }
    }
    derived := strings.Trim(b.String(), "-_.")
    if derived != "" && safeConfigName.MatchString(derived) {
        return derived
    }
    // Fallback: use a short hash suffix to guarantee uniqueness.
    sum := sha256.Sum256([]byte(srcName))
    return "src_" + strings.ToLower(hex.EncodeToString(sum[:4]))
}
