package appbackup

import (
	"fmt"
	"os"
	"sort"
	"strings"

	"gopkg.in/yaml.v3"
)

// R-47 — naming the database SERVICE, not just the running container.
//
// A dump replay must never race the application's own schema management. Proven live on 2026-07-19
// (DIAG-immich-restore-round2-2026-07-19, H4): the reconstitution started the whole stack before
// replaying, immich-server rebuilt `clip_index` two seconds before the dump's own CREATE INDEX, and
// the replay aborted `already exists` under ON_ERROR_STOP=1 — leaving a half-applied schema that the
// app itself then reported as drift. The fix is to bring up ONLY the database service(s) for the
// replay, which requires knowing their compose SERVICE names (docker `up -d <svc>` takes service
// names, not container names).
//
// The symmetry that makes this safe: a `.sql` dump can only exist because DiscoverDatabases matched
// the running container's image string, and the compose `image:` value IS that image string. So the
// same predicate — dbTypeForImage — decides both "is there a dump" and "which service holds it".

// dbTypeForImage maps a container/compose image reference to the database engine the backup code
// supports, or ok=false for anything else (redis/valkey/app images — never started in the DB-only
// phase). Extracted from DiscoverDatabases so the discovery heuristic and the compose heuristic can
// never drift apart; behaviour is byte-equivalent to the inline form it replaced.
func dbTypeForImage(image string) (DBType, bool) {
	img := strings.ToLower(image)
	switch {
	case strings.Contains(img, "postgres"):
		return DBTypePostgres, true
	case strings.Contains(img, "mariadb"), strings.Contains(img, "mysql"):
		return DBTypeMariaDB, true
	}
	return DBType(""), false
}

// composeServicesDoc is the minimal view of a compose file needed here: the `services:` MAP and each
// service's `image:`. Deliberately a real YAML parse and not a line scan — a top-level `volumes:`
// block (immich's `immich_ml_cache:`) has exactly the shape a naive scan misreads as a service, and
// starting a phantom service, or missing the real one, both land in the wrong branch.
type composeServicesDoc struct {
	Services map[string]struct {
		Image string `yaml:"image"`
	} `yaml:"services"`
}

// DBServiceNames returns the sorted compose SERVICE names in composePath whose `image:` identifies a
// supported database engine — the exact argument list for `docker compose up -d <svc>...`.
//
// A file with no (or an empty) `services:` key returns (nil, nil): an app with no identifiable DB
// service is a legitimate, common case and the caller decides what it means. An unreadable or
// unparseable file returns an error, because "cannot tell" must never silently read as "no database"
// — the callers turn that into a refusal when a dump exists.
//
// Image values are matched literally. Catalog templates pin their images literally (enforced since
// Campaign 7), so an interpolated `${...}` image simply does not match and lands in the caller's
// fail-closed branch by design, rather than being guessed at.
func DBServiceNames(composePath string) ([]string, error) {
	data, err := os.ReadFile(composePath)
	if err != nil {
		return nil, fmt.Errorf("reading compose file: %w", err)
	}
	var doc composeServicesDoc
	if err := yaml.Unmarshal(data, &doc); err != nil {
		return nil, fmt.Errorf("parsing compose file %s: %w", composePath, err)
	}
	var names []string
	for name, svc := range doc.Services {
		if _, ok := dbTypeForImage(svc.Image); ok {
			names = append(names, name)
		}
	}
	sort.Strings(names)
	return names, nil
}
