package article

import (
	"fmt"
	"regexp"
	"sort"
	"strings"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
)

type BuildOptions struct {
	ServerModel     string
	SupportCode     string
	ServerPricelist *uint
}

type BuildResult struct {
	Article  string
	Warnings []string
}

var (
	reMemGiB    = regexp.MustCompile(`(?i)(\d+)\s*(GB|G)`)
	reMemTiB    = regexp.MustCompile(`(?i)(\d+)\s*(TB|T)`)
	reCapacityT = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)T`)
	reCapacityG = regexp.MustCompile(`(?i)(\d+(?:[.,]\d+)?)G`)
	rePortSpeed = regexp.MustCompile(`(?i)(\d+)p(\d+)(GbE|G)`)
	rePortFC    = regexp.MustCompile(`(?i)(\d+)pFC(\d+)`)
	reWatts     = regexp.MustCompile(`(?i)(\d{3,5})\s*W`)
)

type namedSeg struct {
	group string // "MODEL","CPU","MEM","GPU","DISK","NET","PSU","SUPPORT"
	value string
}

func Build(local *localdb.LocalDB, items []models.ConfigItem, opts BuildOptions) (BuildResult, error) {
	segs := make([]namedSeg, 0, 8)
	warnings := make([]string, 0)

	model := NormalizeServerModel(opts.ServerModel)
	if model == "" {
		return BuildResult{}, fmt.Errorf("server_model required")
	}
	segs = append(segs, namedSeg{"MODEL", model})

	lotNames := make([]string, 0, len(items))
	for _, it := range items {
		lotNames = append(lotNames, it.LotName)
	}

	if opts.ServerPricelist == nil || *opts.ServerPricelist == 0 {
		return BuildResult{}, fmt.Errorf("pricelist_id required for article")
	}

	cats, err := ResolveLotCategoriesStrict(local, *opts.ServerPricelist, lotNames)
	if err != nil {
		return BuildResult{}, err
	}

	if cpuSeg := buildCPUSegment(items, cats); cpuSeg != "" {
		segs = append(segs, namedSeg{"CPU", cpuSeg})
	}
	memSeg, memWarn := buildMemSegment(items, cats)
	if memWarn != "" {
		warnings = append(warnings, memWarn)
	}
	if memSeg != "" {
		segs = append(segs, namedSeg{"MEM", memSeg})
	}
	if gpuSeg := buildGPUSegment(items, cats); gpuSeg != "" {
		segs = append(segs, namedSeg{"GPU", gpuSeg})
	}
	diskSeg, diskWarn := buildDiskSegment(items, cats)
	if diskWarn != "" {
		warnings = append(warnings, diskWarn)
	}
	if diskSeg != "" {
		segs = append(segs, namedSeg{"DISK", diskSeg})
	}
	netSeg, netWarn := buildNetSegment(items, cats)
	if netWarn != "" {
		warnings = append(warnings, netWarn)
	}
	if netSeg != "" {
		segs = append(segs, namedSeg{"NET", netSeg})
	}
	psuSeg, psuWarn := buildPSUSegment(items, cats)
	if psuWarn != "" {
		warnings = append(warnings, psuWarn)
	}
	if psuSeg != "" {
		segs = append(segs, namedSeg{"PSU", psuSeg})
	}

	if strings.TrimSpace(opts.SupportCode) != "" {
		code := strings.TrimSpace(opts.SupportCode)
		if !isSupportCodeValid(code) {
			return BuildResult{}, fmt.Errorf("invalid_support_code")
		}
		segs = append(segs, namedSeg{"SUPPORT", code})
	}

	article := strings.Join(namedSegsValues(segs), "-")
	if len([]rune(article)) > 80 {
		article = compressArticle(segs)
		warnings = append(warnings, "compressed")
	}
	if len([]rune(article)) > 80 {
		return BuildResult{}, fmt.Errorf("article_overflow")
	}

	return BuildResult{Article: article, Warnings: warnings}, nil
}

func namedSegsValues(segs []namedSeg) []string {
	out := make([]string, len(segs))
	for i, s := range segs {
		out[i] = s.value
	}
	return out
}

func findSegGroup(segs []namedSeg, group string) int {
	for i, s := range segs {
		if s.group == group {
			return i
		}
	}
	return -1
}

func isSupportCodeValid(code string) bool {
	if len(code) < 3 {
		return false
	}
	if !strings.Contains(code, "y") {
		return false
	}
	parts := strings.Split(code, "y")
	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
		return false
	}
	for _, r := range parts[0] {
		if r < '0' || r > '9' {
			return false
		}
	}
	switch parts[1] {
	case "W", "B", "S", "P":
		return true
	default:
		return false
	}
}

func buildCPUSegment(items []models.ConfigItem, cats map[string]string) string {
	type agg struct {
		qty int
	}
	models := map[string]*agg{}
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupCPU {
			continue
		}
		model := parseCPUModel(it.LotName)
		if model == "" {
			model = "UNK"
		}
		if _, ok := models[model]; !ok {
			models[model] = &agg{}
		}
		models[model].qty += it.Quantity
	}
	if len(models) == 0 {
		return ""
	}
	parts := make([]string, 0, len(models))
	for model, a := range models {
		parts = append(parts, fmt.Sprintf("%dx%s", a.qty, model))
	}
	sort.Strings(parts)
	return strings.Join(parts, "+")
}

func buildMemSegment(items []models.ConfigItem, cats map[string]string) (string, string) {
	totalGiB := 0
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupMEM {
			continue
		}
		per := parseMemGiB(it.LotName)
		if per <= 0 {
			return "", "mem_unknown"
		}
		totalGiB += per * it.Quantity
	}
	if totalGiB == 0 {
		return "", ""
	}
	if totalGiB%1024 == 0 {
		return fmt.Sprintf("%dT", totalGiB/1024), ""
	}
	return fmt.Sprintf("%dG", totalGiB), ""
}

func buildGPUSegment(items []models.ConfigItem, cats map[string]string) string {
	models := map[string]int{}
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupGPU {
			continue
		}
		if strings.HasPrefix(strings.ToUpper(it.LotName), "MB_") {
			continue
		}
		model := parseGPUModel(it.LotName)
		if model == "" {
			model = "UNK"
		}
		models[model] += it.Quantity
	}
	if len(models) == 0 {
		return ""
	}
	parts := make([]string, 0, len(models))
	for model, qty := range models {
		parts = append(parts, fmt.Sprintf("%dx%s", qty, model))
	}
	sort.Strings(parts)
	return strings.Join(parts, "+")
}

func buildDiskSegment(items []models.ConfigItem, cats map[string]string) (string, string) {
	type key struct {
		t string
		c string
	}
	groupQty := map[key]int{}
	warn := ""
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupDISK {
			continue
		}
		capToken := parseCapacity(it.LotName)
		if capToken == "" {
			warn = "disk_unknown"
		}
		typeCode := diskTypeCode(cats[it.LotName], it.LotName)
		k := key{t: typeCode, c: capToken}
		groupQty[k] += it.Quantity
	}
	if len(groupQty) == 0 {
		return "", ""
	}
	parts := make([]string, 0, len(groupQty))
	for k, qty := range groupQty {
		if k.c == "" {
			parts = append(parts, fmt.Sprintf("%dx%s", qty, k.t))
		} else {
			parts = append(parts, fmt.Sprintf("%dx%s%s", qty, k.c, k.t))
		}
	}
	sort.Strings(parts)
	return strings.Join(parts, "+"), warn
}

func buildNetSegment(items []models.ConfigItem, cats map[string]string) (string, string) {
	groupQty := map[string]int{}
	warn := ""
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupNET {
			continue
		}
		profile := parsePortSpeed(it.LotName)
		if profile == "" {
			profile = "UNKNET"
			warn = "net_unknown"
		}
		groupQty[profile] += it.Quantity
	}
	if len(groupQty) == 0 {
		return "", ""
	}
	parts := make([]string, 0, len(groupQty))
	for profile, qty := range groupQty {
		parts = append(parts, fmt.Sprintf("%dx%s", qty, profile))
	}
	sort.Strings(parts)
	return strings.Join(parts, "+"), warn
}

func buildPSUSegment(items []models.ConfigItem, cats map[string]string) (string, string) {
	groupQty := map[string]int{}
	warn := ""
	for _, it := range items {
		group, ok := GroupForLotCategory(cats[it.LotName])
		if !ok || group != GroupPSU {
			continue
		}
		rating := parseWatts(it.LotName)
		if rating == "" {
			rating = "UNKPSU"
			warn = "psu_unknown"
		}
		groupQty[rating] += it.Quantity
	}
	if len(groupQty) == 0 {
		return "", ""
	}
	parts := make([]string, 0, len(groupQty))
	for rating, qty := range groupQty {
		parts = append(parts, fmt.Sprintf("%dx%s", qty, rating))
	}
	sort.Strings(parts)
	return strings.Join(parts, "+"), warn
}

func normalizeModelToken(lotName string) string {
	if idx := strings.Index(lotName, "_"); idx >= 0 && idx+1 < len(lotName) {
		lotName = lotName[idx+1:]
	}
	parts := strings.Split(lotName, "_")
	token := parts[len(parts)-1]
	return strings.ToUpper(strings.TrimSpace(token))
}

func parseCPUModel(lotName string) string {
	parts := strings.Split(lotName, "_")
	if len(parts) >= 2 {
		last := strings.ToUpper(strings.TrimSpace(parts[len(parts)-1]))
		if last != "" {
			return last
		}
	}
	return normalizeModelToken(lotName)
}

func parseGPUModel(lotName string) string {
	upper := strings.ToUpper(lotName)
	if idx := strings.Index(upper, "GPU_"); idx >= 0 {
		upper = upper[idx+4:]
	}
	parts := strings.Split(upper, "_")
	model := ""
	numSuffix := ""
	mem := ""
	for i, p := range parts {
		if p == "" {
			continue
		}
		switch p {
		case "NV", "NVIDIA", "INTEL", "AMD", "RADEON", "PCIE", "PCI", "SXM", "SXMX", "SFF", "LOVELACE":
			continue
		case "ADA", "AMPERE", "HOPPER", "BLACKWELL":
			if model != "" {
				archAbbr := map[string]string{
					"ADA": "ADA", "AMPERE": "AMP", "HOPPER": "HOP", "BLACKWELL": "BWL",
				}
				numSuffix += archAbbr[p]
			}
			continue
		default:
			if strings.Contains(p, "GB") {
				mem = p
				continue
			}
			if model == "" && i > 0 {
				model = p
			} else if model != "" && numSuffix == "" && isNumeric(p) {
				numSuffix = p
			}
		}
	}
	full := model
	if numSuffix != "" {
		full = model + numSuffix
	}
	if full != "" && mem != "" {
		return full + "_" + mem
	}
	if full != "" {
		return full
	}
	return normalizeModelToken(lotName)
}

func isNumeric(s string) bool {
	if s == "" {
		return false
	}
	for _, r := range s {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func parseMemGiB(lotName string) int {
	if m := reMemTiB.FindStringSubmatch(lotName); len(m) == 3 {
		return atoi(m[1]) * 1024
	}
	if m := reMemGiB.FindStringSubmatch(lotName); len(m) == 3 {
		return atoi(m[1])
	}
	return 0
}

func parseCapacity(lotName string) string {
	if m := reCapacityT.FindStringSubmatch(lotName); len(m) == 2 {
		return normalizeTToken(strings.ReplaceAll(m[1], ",", ".")) + "T"
	}
	if m := reCapacityG.FindStringSubmatch(lotName); len(m) == 2 {
		return normalizeNumberToken(strings.ReplaceAll(m[1], ",", ".")) + "G"
	}
	return ""
}

func diskTypeCode(cat string, lotName string) string {
	c := strings.ToUpper(strings.TrimSpace(cat))
	if c == "M2" {
		return "M2"
	}
	upper := strings.ToUpper(lotName)
	if strings.Contains(upper, "NVME") {
		return "NV"
	}
	if strings.Contains(upper, "SAS") {
		return "SAS"
	}
	if strings.Contains(upper, "SATA") {
		return "SAT"
	}
	return c
}

func parsePortSpeed(lotName string) string {
	if m := rePortSpeed.FindStringSubmatch(lotName); len(m) == 4 {
		return fmt.Sprintf("%sp%sG", m[1], m[2])
	}
	if m := rePortFC.FindStringSubmatch(lotName); len(m) == 3 {
		return fmt.Sprintf("%spFC%s", m[1], m[2])
	}
	return ""
}

func parseWatts(lotName string) string {
	if m := reWatts.FindStringSubmatch(lotName); len(m) == 2 {
		w := atoi(m[1])
		if w >= 1000 {
			kw := fmt.Sprintf("%.1f", float64(w)/1000.0)
			kw = strings.TrimSuffix(kw, ".0")
			return fmt.Sprintf("%skW", kw)
		}
		return fmt.Sprintf("%dW", w)
	}
	return ""
}

func normalizeNumberToken(raw string) string {
	raw = strings.TrimSpace(raw)
	raw = strings.TrimLeft(raw, "0")
	if raw == "" || raw[0] == '.' {
		raw = "0" + raw
	}
	return raw
}

func normalizeTToken(raw string) string {
	raw = normalizeNumberToken(raw)
	parts := strings.SplitN(raw, ".", 2)
	intPart := parts[0]
	frac := ""
	if len(parts) == 2 {
		frac = parts[1]
	}
	if frac == "" {
		frac = "0"
	}
	if len(intPart) >= 2 {
		return intPart + "." + frac
	}
	if len(frac) > 1 {
		frac = frac[:1]
	}
	return intPart + "." + frac
}

func atoi(v string) int {
	n := 0
	for _, r := range v {
		if r < '0' || r > '9' {
			continue
		}
		n = n*10 + int(r-'0')
	}
	return n
}

func compressArticle(segs []namedSeg) string {
	if len(segs) == 0 {
		return ""
	}
	for i, s := range segs {
		segs[i].value = strings.ReplaceAll(s.value, "GbE", "G")
	}
	article := strings.Join(namedSegsValues(segs), "-")
	if len([]rune(article)) <= 80 {
		return article
	}

	// 1) remove PSU
	if i := findSegGroup(segs, "PSU"); i >= 0 {
		segs = append(segs[:i], segs[i+1:]...)
		article = strings.Join(namedSegsValues(segs), "-")
		if len([]rune(article)) <= 80 {
			return article
		}
	}

	// 2) compress NET/HBA/HCA
	if i := findSegGroup(segs, "NET"); i >= 0 {
		segs[i].value = compressNetSegment(segs[i].value)
		article = strings.Join(namedSegsValues(segs), "-")
		if len([]rune(article)) <= 80 {
			return article
		}
	}

	// 3) compress DISK
	if i := findSegGroup(segs, "DISK"); i >= 0 {
		segs[i].value = compressDiskSegment(segs[i].value)
		article = strings.Join(namedSegsValues(segs), "-")
		if len([]rune(article)) <= 80 {
			return article
		}
	}

	// 4) compress GPU to vendor only (GPU_NV)
	if i := findSegGroup(segs, "GPU"); i >= 0 {
		segs[i].value = compressGPUSegment(segs[i].value)
	}
	return strings.Join(namedSegsValues(segs), "-")
}

func compressNetSegment(seg string) string {
	if seg == "" {
		return seg
	}
	parts := strings.Split(seg, "+")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		qty := "1"
		profile := p
		if x := strings.SplitN(p, "x", 2); len(x) == 2 {
			qty = x[0]
			profile = x[1]
		}
		upper := strings.ToUpper(profile)
		label := "NIC"
		if strings.Contains(upper, "FC") {
			label = "HBA"
		} else if strings.Contains(upper, "HCA") || strings.Contains(upper, "IB") {
			label = "HCA"
		}
		out = append(out, fmt.Sprintf("%sx%s", qty, label))
	}
	if len(out) == 0 {
		return seg
	}
	sort.Strings(out)
	return strings.Join(out, "+")
}

func compressDiskSegment(seg string) string {
	if seg == "" {
		return seg
	}
	parts := strings.Split(seg, "+")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		qty := "1"
		spec := p
		if x := strings.SplitN(p, "x", 2); len(x) == 2 {
			qty = x[0]
			spec = x[1]
		}
		upper := strings.ToUpper(spec)
		label := "DSK"
		for _, t := range []string{"M2", "NV", "SAS", "SAT", "SSD", "HDD", "EDS", "HHH"} {
			if strings.Contains(upper, t) {
				label = t
				break
			}
		}
		out = append(out, fmt.Sprintf("%sx%s", qty, label))
	}
	if len(out) == 0 {
		return seg
	}
	sort.Strings(out)
	return strings.Join(out, "+")
}

func compressGPUSegment(seg string) string {
	if seg == "" {
		return seg
	}
	parts := strings.Split(seg, "+")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		qty := "1"
		if x := strings.SplitN(p, "x", 2); len(x) == 2 {
			qty = x[0]
		}
		out = append(out, fmt.Sprintf("%sxGPU_NV", qty))
	}
	if len(out) == 0 {
		return seg
	}
	sort.Strings(out)
	return strings.Join(out, "+")
}
