package stacks

import (
	"bufio"
	"os"
	"path"
	"strings"

	"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)

// composeVarRoots maps the two deploy-time path variables to their classification root. A bind's
// host side must reference one of these LITERALLY (the classifier works in ${VAR}-relative space —
// it does NOT resolve envs, unlike ParseComposeHDDMounts). ${USERDATA_PATH} is <HDD_PATH>/userdata
// at deploy, but in the compose file the two are written distinctly, so they are distinct roots.
var composeVarRoots = []struct {
	varRef string
	root   appbackup.BindRoot
}{
	{"${USERDATA_PATH}", appbackup.RootUserdata},
	{"${HDD_PATH}", appbackup.RootHDD},
	// ${IMPORT_PATH} (R-75) is the canonical drop-zone root on the SYSTEM drive. It must be listed
	// BEFORE any prefix of it could match — it shares no prefix with the other two, so order is not
	// load-bearing here, but the classifier works in ${VAR}-relative space and never resolves envs,
	// so this entry is what makes an ingest bind classifiable at all.
	{"${IMPORT_PATH}", appbackup.RootImport},
}

// ParseComposeClassifiableBinds extracts the ${HDD_PATH}/${USERDATA_PATH}-relative host binds from a
// docker-compose.yml, for backup classification (Part 2 of the classification arc). It copies the
// ParseComposeUserdataMounts scanner shape (service-level `volumes:` section, `- ` short-syntax
// lines, quote-trim, `SplitN(":",3)`) but stays in RELATIVE ${VAR} space and preserves the `:ro`
// flag — both of which the classifier needs and which ParseComposeHDDMounts discards.
//
// RelPath is the path.Clean'd remainder after the variable (leading "/" stripped; "" for a bare-root
// bind). ReadOnly is true iff the mode field (parts[2]) contains a `ro` token. Deduped on
// (Root, RelPath) — the FIRST occurrence's ReadOnly wins (the catalog never mixes modes for one
// path; noted so a future mixed case is a conscious change, not a silent one).
//
// Long-syntax volumes (`type: bind`) are NOT supported — parity with every existing compose parser;
// the catalog uses short syntax only. Pure given the file bytes (no env resolution, no FS beyond the
// read).
func ParseComposeClassifiableBinds(composePath string) []appbackup.ComposeBind {
	data, err := os.ReadFile(composePath)
	if err != nil {
		return nil
	}
	var binds []appbackup.ComposeBind
	seen := make(map[string]bool) // "<root>\x00<relpath>"

	scanner := bufio.NewScanner(strings.NewReader(string(data)))
	inVolumes := false
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if strings.HasPrefix(line, "volumes:") {
			inVolumes = true
			continue
		}
		if inVolumes && !strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "#") && line != "" {
			inVolumes = false
		}
		if !inVolumes || !strings.HasPrefix(line, "- ") {
			continue
		}
		mountStr := strings.Trim(strings.TrimPrefix(line, "- "), "\"'")
		parts := strings.SplitN(mountStr, ":", 3)
		if len(parts) < 2 {
			continue
		}
		root, relPath, ok := classifyRoot(parts[0])
		if !ok {
			continue
		}
		readOnly := len(parts) == 3 && modeIsReadOnly(parts[2])
		key := string(root) + "\x00" + relPath
		if seen[key] {
			continue // first occurrence's ReadOnly wins
		}
		seen[key] = true
		binds = append(binds, appbackup.ComposeBind{Root: root, RelPath: relPath, ReadOnly: readOnly})
	}
	return binds
}

// classifyRoot resolves a compose host-side token to its (root, relpath) if it references one of the
// classification variables exactly or as a "/"-separated prefix. relPath is path.Clean'd, "" for a
// bare root.
func classifyRoot(hostPath string) (appbackup.BindRoot, string, bool) {
	for _, v := range composeVarRoots {
		var rem string
		switch {
		case hostPath == v.varRef:
			rem = ""
		case strings.HasPrefix(hostPath, v.varRef+"/"):
			rem = strings.TrimPrefix(hostPath, v.varRef+"/")
		default:
			continue
		}
		if rem == "" {
			return v.root, "", true
		}
		rel := path.Clean(rem)
		if rel == "." {
			rel = ""
		}
		return v.root, rel, true
	}
	return "", "", false
}

// modeIsReadOnly reports whether a docker volume mode field (e.g. "ro", "rw", "ro,z", "z") carries a
// `ro` token.
func modeIsReadOnly(mode string) bool {
	for _, tok := range strings.Split(mode, ",") {
		if strings.TrimSpace(tok) == "ro" {
			return true
		}
	}
	return false
}
