package providers

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
)

// Mango window rules are flat `windowrule=key:value,...` lines. DMS-managed rules
// live in dms/windowrules.conf (sourced from config.conf), each preceded by an
// `# @id=<id> @name=<name>` comment so they round-trip.

type MangoWindowRule struct {
	Source string
	Fields map[string]string
}

var mangoWindowRuleRegex = regexp.MustCompile(`^windowrule\s*=\s*(.+)$`)
var mangoMetaCommentRegex = regexp.MustCompile(`^#\s*@id=(\S*)\s*@name=(.*)$`)

func parseMangoWindowRuleLine(value string) map[string]string {
	fields := map[string]string{}
	for _, pair := range strings.Split(value, ",") {
		pair = strings.TrimSpace(pair)
		if pair == "" {
			continue
		}
		colon := strings.Index(pair, ":")
		if colon < 0 {
			continue
		}
		key := strings.TrimSpace(pair[:colon])
		val := strings.TrimSpace(pair[colon+1:])
		if key != "" {
			fields[key] = val
		}
	}
	return fields
}

// mangoConfigPath returns the main mango config (config.conf or mango.conf).
func mangoConfigPath(configDir string) string {
	candidates := []string{
		filepath.Join(configDir, "config.conf"),
		filepath.Join(configDir, "mango.conf"),
	}
	for _, c := range candidates {
		if _, err := os.Stat(c); err == nil {
			return c
		}
	}
	return candidates[0]
}

func mangoOverridePath(configDir string) string {
	return filepath.Join(configDir, "dms", "windowrules.conf")
}

// parseMangoRulesFile reads a config file and returns its windowrule= lines.
func parseMangoRulesFile(path, source string) []MangoWindowRule {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil
	}
	var rules []MangoWindowRule
	for _, line := range strings.Split(string(data), "\n") {
		trimmed := strings.TrimSpace(line)
		if m := mangoWindowRuleRegex.FindStringSubmatch(trimmed); m != nil {
			rules = append(rules, MangoWindowRule{Source: source, Fields: parseMangoWindowRuleLine(m[1])})
		}
	}
	return rules
}

type MangoRulesParseResult struct {
	Rules            []MangoWindowRule
	DMSRulesIncluded bool
	DMSStatus        *windowrules.DMSRulesStatus
}

func ParseMangoWindowRules(configDir string) (*MangoRulesParseResult, error) {
	mainPath := mangoConfigPath(configDir)
	overridePath := mangoOverridePath(configDir)

	var rules []MangoWindowRule
	rules = append(rules, parseMangoRulesFile(mainPath, "config.conf")...)
	rules = append(rules, parseMangoRulesFile(overridePath, "dms/windowrules.conf")...)

	included := mangoDMSRulesIncluded(mainPath)
	return &MangoRulesParseResult{
		Rules:            rules,
		DMSRulesIncluded: included,
		DMSStatus: &windowrules.DMSRulesStatus{
			Exists:        fileExists(overridePath),
			Included:      included,
			Effective:     included,
			ConfigFormat:  "conf",
			StatusMessage: mangoIncludeMessage(included),
		},
	}, nil
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func mangoDMSRulesIncluded(mainPath string) bool {
	data, err := os.ReadFile(mainPath)
	if err != nil {
		return false
	}
	for _, line := range strings.Split(string(data), "\n") {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "source") && strings.Contains(trimmed, "dms/windowrules.conf") {
			return true
		}
	}
	return false
}

func mangoIncludeMessage(included bool) string {
	if included {
		return "DMS window rules are sourced from config.conf"
	}
	return "Add `source=./dms/windowrules.conf` to config.conf to apply DMS window rules"
}

func mangoBoolField(fields map[string]string, key string) *bool {
	v, ok := fields[key]
	if !ok {
		return nil
	}
	b := v == "1" || strings.EqualFold(v, "true")
	return &b
}

func mangoBoolStr(b *bool) string {
	if b != nil && *b {
		return "1"
	}
	return "0"
}

func ConvertMangoRulesToWindowRules(mangoRules []MangoWindowRule) []windowrules.WindowRule {
	result := make([]windowrules.WindowRule, 0, len(mangoRules))
	for i, mr := range mangoRules {
		f := mr.Fields
		actions := windowrules.Actions{
			OpenFloating:   mangoBoolField(f, "isfloating"),
			OpenFullscreen: mangoBoolField(f, "isfullscreen"),
			NoBlur:         mangoBoolField(f, "noblur"),
			NoBorder:       mangoBoolField(f, "isnoborder"),
			NoShadow:       mangoBoolField(f, "isnoshadow"),
			NoRounding:     mangoBoolField(f, "isnoradius"),
			NoAnim:         mangoBoolField(f, "isnoanimation"),
		}
		if tags, ok := f["tags"]; ok {
			actions.Workspace = tags
		}
		if mon, ok := f["monitor"]; ok {
			actions.Monitor = mon
		}
		if w, ok := f["width"]; ok {
			if h, ok2 := f["height"]; ok2 {
				actions.Size = w + "x" + h
			}
		}

		result = append(result, windowrules.WindowRule{
			ID:      fmt.Sprintf("rule_%d", i),
			Enabled: true,
			Source:  mr.Source,
			MatchCriteria: windowrules.MatchCriteria{
				AppID: f["appid"],
				Title: f["title"],
			},
			Actions: actions,
		})
	}
	return result
}

// formatMangoRule serializes a shared WindowRule into a mango windowrule= line.
func formatMangoRule(rule windowrules.WindowRule) string {
	var parts []string
	add := func(k, v string) {
		if v != "" {
			parts = append(parts, k+":"+v)
		}
	}

	add("appid", rule.MatchCriteria.AppID)
	add("title", rule.MatchCriteria.Title)
	add("tags", rule.Actions.Workspace)
	add("monitor", rule.Actions.Monitor)

	if rule.Actions.Size != "" {
		if w, h, ok := splitSize(rule.Actions.Size); ok {
			add("width", w)
			add("height", h)
		}
	}

	addBool := func(k string, b *bool) {
		if b != nil {
			parts = append(parts, k+":"+mangoBoolStr(b))
		}
	}
	addBool("isfloating", rule.Actions.OpenFloating)
	addBool("isfullscreen", rule.Actions.OpenFullscreen)
	addBool("noblur", rule.Actions.NoBlur)
	addBool("isnoborder", rule.Actions.NoBorder)
	addBool("isnoshadow", rule.Actions.NoShadow)
	addBool("isnoradius", rule.Actions.NoRounding)
	addBool("isnoanimation", rule.Actions.NoAnim)

	return "windowrule=" + strings.Join(parts, ",")
}

func splitSize(size string) (w, h string, ok bool) {
	for _, sep := range []string{"x", "X", " "} {
		if parts := strings.Split(size, sep); len(parts) == 2 {
			w = strings.TrimSpace(parts[0])
			h = strings.TrimSpace(parts[1])
			if _, err := strconv.ParseFloat(w, 64); err == nil {
				return w, h, true
			}
		}
	}
	return "", "", false
}

type MangoWritableProvider struct {
	configDir string
}

func NewMangoWritableProvider(configDir string) *MangoWritableProvider {
	return &MangoWritableProvider{configDir: configDir}
}

func (p *MangoWritableProvider) Name() string { return "mango" }

func (p *MangoWritableProvider) GetOverridePath() string {
	return mangoOverridePath(p.configDir)
}

func (p *MangoWritableProvider) GetRuleSet() (*windowrules.RuleSet, error) {
	result, err := ParseMangoWindowRules(p.configDir)
	if err != nil {
		return nil, err
	}
	return &windowrules.RuleSet{
		Title:            "Mango Window Rules",
		Provider:         "mango",
		Rules:            ConvertMangoRulesToWindowRules(result.Rules),
		DMSRulesIncluded: result.DMSRulesIncluded,
		DMSStatus:        result.DMSStatus,
	}, nil
}

func (p *MangoWritableProvider) SetRule(rule windowrules.WindowRule) error {
	rules, err := p.LoadDMSRules()
	if err != nil {
		rules = []windowrules.WindowRule{}
	}
	found := false
	for i, r := range rules {
		if r.ID == rule.ID {
			rules[i] = rule
			found = true
			break
		}
	}
	if !found {
		rules = append(rules, rule)
	}
	return p.writeDMSRules(rules)
}

func (p *MangoWritableProvider) RemoveRule(id string) error {
	rules, err := p.LoadDMSRules()
	if err != nil {
		return err
	}
	newRules := make([]windowrules.WindowRule, 0, len(rules))
	for _, r := range rules {
		if r.ID != id {
			newRules = append(newRules, r)
		}
	}
	return p.writeDMSRules(newRules)
}

func (p *MangoWritableProvider) ReorderRules(ids []string) error {
	rules, err := p.LoadDMSRules()
	if err != nil {
		return err
	}
	ruleMap := make(map[string]windowrules.WindowRule, len(rules))
	for _, r := range rules {
		ruleMap[r.ID] = r
	}
	newRules := make([]windowrules.WindowRule, 0, len(ids))
	for _, id := range ids {
		if r, ok := ruleMap[id]; ok {
			newRules = append(newRules, r)
			delete(ruleMap, id)
		}
	}
	for _, r := range ruleMap {
		newRules = append(newRules, r)
	}
	return p.writeDMSRules(newRules)
}

// LoadDMSRules parses only the DMS override file, preserving @id/@name metadata.
func (p *MangoWritableProvider) LoadDMSRules() ([]windowrules.WindowRule, error) {
	data, err := os.ReadFile(p.GetOverridePath())
	if err != nil {
		if os.IsNotExist(err) {
			return []windowrules.WindowRule{}, nil
		}
		return nil, err
	}

	var rules []windowrules.WindowRule
	var curID, curName string
	idx := 0
	for _, line := range strings.Split(string(data), "\n") {
		trimmed := strings.TrimSpace(line)
		if m := mangoMetaCommentRegex.FindStringSubmatch(trimmed); m != nil {
			curID = m[1]
			curName = strings.TrimSpace(m[2])
			continue
		}
		if m := mangoWindowRuleRegex.FindStringSubmatch(trimmed); m != nil {
			converted := ConvertMangoRulesToWindowRules([]MangoWindowRule{{Source: "dms/windowrules.conf", Fields: parseMangoWindowRuleLine(m[1])}})
			wr := converted[0]
			if curID != "" {
				wr.ID = curID
			} else {
				wr.ID = fmt.Sprintf("rule_%d", idx)
			}
			wr.Name = curName
			rules = append(rules, wr)
			curID, curName = "", ""
			idx++
		}
	}
	return rules, nil
}

func (p *MangoWritableProvider) writeDMSRules(rules []windowrules.WindowRule) error {
	overridePath := p.GetOverridePath()
	if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
		return err
	}

	var sb strings.Builder
	sb.WriteString("# Auto-generated by DMS - DMS-managed mango window rules\n\n")
	for i, r := range rules {
		id := r.ID
		if id == "" {
			id = fmt.Sprintf("rule_%d", i)
		}
		fmt.Fprintf(&sb, "# @id=%s @name=%s\n", id, r.Name)
		sb.WriteString(formatMangoRule(r))
		sb.WriteString("\n\n")
	}

	return os.WriteFile(overridePath, []byte(sb.String()), 0o644)
}
