package config

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

	"deepin-power-control/internal/features"
	"deepin-power-control/internal/global"
	"deepin-power-control/internal/rules"
	"deepin-power-control/internal/utils"

	"gopkg.in/ini.v1"
)

// featureEngine evaluates feature conditions for both config files
// (@feature filename tags) and rules (match.feature). Declarations come
// from embedded features.yaml; anything not declared falls back to the
// code registry in the features package. Tests mock behavior by swapping
// features.FeatureChecks rather than this engine.
var featureEngine = features.New()

// ConfigValue stores the value and its origin file for tracing
type ConfigValue struct {
	Value   string
	Source  string
	Section string
}

// ApplyMode merges configurations and applies them to TLP
func ApplyMode(mode string) error {
	global.Info("Starting configuration merge for mode: %s", mode)

	// 1. Scan and retrieve all configuration files
	files, err := utils.GetConfigFilesInDir(global.RawConfigDir)
	if err != nil {
		return fmt.Errorf("configuration scan failed: %w", err)
	}

	mergedMap := make(map[string]ConfigValue)

	// 2. Iterate through files and merge content
	for _, file := range files {
		if err := processSingleFile(file, mode, mergedMap); err != nil {
			global.Warn("Skipping file %s: %v", file, err)
			continue
		}
	}

	// 3. Match rules once, then merge config fragments (after files, so
	// rules override base configs; later rules override earlier ones).
	// Fragments are rendered with a [mode] section header, so they flow
	// through the standard section-based merge path with no special casing.
	matched := rules.MatchedRules(mode, featureEngine)
	for _, frag := range rules.CollectConfigFragments(matched) {
		src := Source{
			Name:    "rule: " + frag.Source,
			Content: renderConfigFragment(frag, mode),
		}
		if err := processSource(src, mode, mergedMap); err != nil {
			global.Warn("Skipping rule fragment from %s: %v", frag.Source, err)
			continue
		}
	}

	// 4. Persist the merged configuration to TLP directory
	if err := writeMergedConfig(mergedMap, mode); err != nil {
		return err
	}

	// 5. Execute write/exec rules (after the TLP config is persisted).
	// Config actions were already collected in step 3 and are skipped here.
	rules.ExecuteActions(matched)
	return nil
}

// Source is a config input: either a file on disk or an in-memory fragment
// generated by rules. Files are read into Content before processing so the
// merge logic treats both uniformly.
type Source struct {
	Name    string // display name (file path or "rule: <rule-file>")
	Content []byte
}

// processSource handles the logic for a single config source
func processSource(src Source, mode string, mergedMap map[string]ConfigValue) error {
	name := src.Name
	fileName := filepath.Base(name)

	// Load INI content
	cfg, err := ini.Load(src.Content)
	if err != nil {
		return fmt.Errorf("load failed: %v", err)
	}

	if hasSections(cfg) {
		// Format A: Section-based ([DEFAULT] + [mode])
		global.Debug("Source %s: Section-based format detected", name)
		mergeSection(cfg.Section(""), fileName, mergedMap)
		if sec, err := cfg.GetSection(mode); err == nil {
			mergeSection(sec, fileName, mergedMap)
		}
	} else {
		// Format B: Filename-based or Global
		global.Debug("Source %s: No sections detected, checking filename...", name)
		targetMode := getModeFromFilename(fileName)
		if targetMode == "" || targetMode == mode {
			mergeSection(cfg.Section(""), fileName, mergedMap)
		} else {
			global.Debug("Source %s skipped: Mode mismatch (%s != %s)", name, targetMode, mode)
		}
	}

	return nil
}

// processSingleFile handles the logic for a single configuration file.
// The hardware feature check runs here (before reading the file), so
// rule fragments never enter this path and need no source marker.
func processSingleFile(path string, mode string, mergedMap map[string]ConfigValue) error {
	fileName := filepath.Base(path)
	if !shouldApplyFile(fileName) {
		global.Debug("File %s skipped: Hardware feature not supported", fileName)
		return nil
	}

	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read failed: %v", err)
	}
	return processSource(Source{Name: path, Content: data}, mode, mergedMap)
}

// renderConfigFragment serializes a rule config fragment into INI content
// with a [mode] section header, so it flows through the standard
// section-based merge path (Format A) without any special casing.
func renderConfigFragment(frag rules.ConfigFragment, mode string) []byte {
	keys := make([]string, 0, len(frag.Items))
	for k := range frag.Items {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	var b strings.Builder
	b.WriteString("# Generated by rule: " + frag.Source + "\n")
	b.WriteString("[" + mode + "]\n")
	for _, k := range keys {
		b.WriteString(k + "=" + frag.Items[k] + "\n")
	}
	return []byte(b.String())
}

// shouldApplyFile parses the @feature tag and checks hardware compatibility
func shouldApplyFile(fileName string) bool {
	// Pattern: filename@feature.conf
	if !strings.Contains(fileName, "@") {
		return true
	}

	// Extract feature name between '@' and '.conf'
	parts := strings.Split(fileName, "@")
	if len(parts) < 2 {
		return true
	}
	featurePart := strings.TrimSuffix(parts[1], ".conf")

	// Consult the feature engine: declared features (features.yaml) first,
	// then the legacy registries as implicit fallback. Parameterized names
	// like "@pci:1002:699f" are handled by the engine's parameterized
	// registry - no per-feature special case here.
	return featureEngine.Enabled(featurePart)
}

// hasSections checks if the INI file contains any non-default sections
func hasSections(cfg *ini.File) bool {
	return len(cfg.Sections()) > 1
}

// ValidModes lists the supported power modes.
var ValidModes = []string{"performance", "balance", "saving", "lowbat"}

// IsValidMode reports whether the given mode is one of the supported modes.
func IsValidMode(mode string) bool {
	for _, m := range ValidModes {
		if m == mode {
			return true
		}
	}
	return false
}

// getModeFromFilename extracts mode from name like "xxx_performance.conf" "xxx-performance.conf" "performance.conf"
func getModeFromFilename(name string) string {
	baseName := strings.TrimSuffix(name, ".conf")
	for _, m := range ValidModes {
		if baseName == m || strings.Contains(baseName, "_"+m) || strings.Contains(baseName, "-"+m) {
			return m
		}
	}
	return ""
}

// mergeSection copies keys from an INI section to our result map
func mergeSection(section *ini.Section, sourceName string, res map[string]ConfigValue) {
	sectionName := section.Name()
	for _, key := range section.Keys() {
		res[key.Name()] = ConfigValue{
			Value:   key.String(),
			Source:  sourceName,
			Section: sectionName,
		}
	}
}

// writeMergedConfig handles file cleanup and generation of the final .conf file
func writeMergedConfig(mergedMap map[string]ConfigValue, mode string) error {
	// 1. Clean up old dspc-related config files in /etc/tlp.d/
	existingFiles, _ := filepath.Glob(filepath.Join(global.TlpConfigDir, "*.conf"))
	for _, f := range existingFiles {
		base := filepath.Base(f)
		if strings.Contains(base, "deepin-system-power-control") || strings.Contains(base, "dspc") || strings.Contains(base, "deepin-power-control") {
			global.Debug("Removing old config: %s", base)
			if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
				// Key point: Deletion failure should be treated as an error, preventing subsequent writes.
				return fmt.Errorf("failed to remove old config file %s: %w", f, err)
			}
		}
	}

	// 2. Create the target file
	targetFileName := fmt.Sprintf("%s_%s.conf", global.TargetTlpFilePrefix, mode)
	targetPath := filepath.Join(global.TlpConfigDir, targetFileName)
	f, err := os.Create(targetPath)
	if err != nil {
		return fmt.Errorf("failed to create target file: %w", err)
	}
	defer f.Close()

	writer := bufio.NewWriter(f)
	_, _ = writer.WriteString("# This file is auto-generated by deepin-power-control. DO NOT EDIT.\n")

	// 3. Group keys by source to minimize redundant comments
	// First, get sorted keys to ensure stable output
	keys := make([]string, 0, len(mergedMap))
	for k := range mergedMap {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// Source -> Section -> []Keys
	hierarchy := make(map[string]map[string][]string)

	for k, cv := range mergedMap {
		if hierarchy[cv.Source] == nil {
			hierarchy[cv.Source] = make(map[string][]string)
		}
		hierarchy[cv.Source][cv.Section] = append(hierarchy[cv.Source][cv.Section], k)
	}

	// A. sort Source (file name)
	sources := make([]string, 0, len(hierarchy))
	for src := range hierarchy {
		sources = append(sources, src)
	}
	sort.Strings(sources)

	for _, src := range sources {
		_, _ = writer.WriteString(fmt.Sprintf("\n# >>> From File: %s\n", src))

		// B. sort Section
		sections := make([]string, 0, len(hierarchy[src]))
		for sec := range hierarchy[src] {
			sections = append(sections, sec)
		}

		sort.Slice(sections, func(i, j int) bool {
			if sections[i] == "DEFAULT" {
				return true
			}
			if sections[j] == "DEFAULT" {
				return false
			}
			return sections[i] < sections[j]
		})

		for _, sec := range sections {
			_, _ = writer.WriteString(fmt.Sprintf("# [%s]\n", sec))

			keys := hierarchy[src][sec]
			sort.Strings(keys)
			for _, k := range keys {
				_, _ = writer.WriteString(fmt.Sprintf("%s=%s\n", k, mergedMap[k].Value))
			}
			_, _ = writer.WriteString("\n")
		}
	}

	return writer.Flush()
}

// InitStart calls 'tlp init start' to reload TLP without config modification
func InitStart() error {
	global.Info("Running TLP init-start...")
	// Use the utility to run: /usr/sbin/tlp init start
	return utils.RunCmd(global.CmdTlp, "init", "start")
}
