// Package preview implements branch-pattern preview deploys. A "template"
// workload is one whose git trigger has a BranchPattern configured; when
// an inbound push event names a branch other than the template's primary
// Branch, the dispatcher materializes (or reuses) a child workload via
// MaterializeForBranch and dispatches the deploy against the child. The
// child is then torn down on a matching branch-delete event.
//
// The package is intentionally narrow:
//   - it does not know about Docker, the proxy, or any plugin internals
//   - it operates over a Store interface so the webhook handler can mock
//     it in tests
//   - it owns the per-branch naming + subdomain mangling so the wiring
//     code (trigger fan-out) stays a pure dispatch path
package preview

import (
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	"github.com/alexei/tinyforge/internal/store"
)

// Store is the slice of the persistence layer the preview package needs.
// Defined locally so tests can fake it without dragging the full Store.
type Store interface {
	GetWorkloadByID(id string) (store.Workload, error)
	ListChildrenByParent(parentID string) ([]store.Workload, error)
	CreateWorkload(w store.Workload) (store.Workload, error)
	DeleteWorkload(id string) error
}

// branchSlugPattern strips characters that are unsafe inside a Docker
// container name, hostname label, or filesystem path. Compiled once.
var branchSlugPattern = regexp.MustCompile(`[^a-z0-9-]+`)

// slugifyBranch converts a git ref-component into a safe slug. Lowercase,
// hyphen-only, length-capped to 32 so name + slug fit inside the Docker
// 63-char container-name and 63-char DNS-label limits with room for the
// `tf-build-` prefix.
func slugifyBranch(branch string) string {
	b := strings.ToLower(branch)
	b = strings.ReplaceAll(b, "/", "-")
	b = branchSlugPattern.ReplaceAllString(b, "-")
	b = strings.Trim(b, "-")
	if b == "" {
		return "branch"
	}
	if len(b) > 32 {
		b = strings.Trim(b[:32], "-")
		if b == "" {
			b = "branch"
		}
	}
	return b
}

// findExistingPreview returns the child workload whose source_config
// already names `branch`, if any. Linear scan over the children list —
// fine because the bound is "branches a single team keeps open at once"
// which is in the dozens, not thousands.
func findExistingPreview(children []store.Workload, branch string) (store.Workload, bool) {
	for _, c := range children {
		var cfg struct {
			Branch string `json:"branch"`
		}
		if c.SourceConfig != "" {
			_ = json.Unmarshal([]byte(c.SourceConfig), &cfg)
		}
		if cfg.Branch == branch {
			return c, true
		}
	}
	return store.Workload{}, false
}

// patchSourceConfigBranch returns a copy of the template's source_config
// with the `branch` field replaced. Unknown keys round-trip so plugin-
// specific config (port, dockerfile path, storage settings, ...) survive.
// A malformed source_config is replaced rather than propagated so the
// preview workload has a clean baseline.
func patchSourceConfigBranch(sourceConfig, branch string) (string, error) {
	if branch == "" {
		return "", fmt.Errorf("preview: branch is empty")
	}
	m := map[string]json.RawMessage{}
	if sourceConfig != "" && sourceConfig != "{}" {
		if err := json.Unmarshal([]byte(sourceConfig), &m); err != nil {
			m = map[string]json.RawMessage{}
		}
	}
	enc, err := json.Marshal(branch)
	if err != nil {
		return "", fmt.Errorf("preview: encode branch: %w", err)
	}
	m["branch"] = enc
	out, err := json.Marshal(m)
	if err != nil {
		return "", fmt.Errorf("preview: encode source_config: %w", err)
	}
	return string(out), nil
}

// patchPublicFacesSubdomain prefixes every public face's Subdomain with
// the branch slug so two preview deploys never collide on the same FQDN.
// Faces with no subdomain are left untouched — the operator clearly
// didn't want a per-branch host carved out for that face.
func patchPublicFacesSubdomain(publicFaces, slug string) (string, error) {
	if publicFaces == "" || publicFaces == "[]" {
		return publicFaces, nil
	}
	var faces []map[string]any
	if err := json.Unmarshal([]byte(publicFaces), &faces); err != nil {
		// Malformed faces MUST fail loudly: returning the template's faces
		// verbatim would give the preview the SAME subdomains as the
		// template, so the preview's proxy route would clobber the template's
		// (the exact collision the slug prefix exists to prevent).
		return "", fmt.Errorf("preview: parse public_faces: %w", err)
	}
	for _, f := range faces {
		sub, ok := f["subdomain"].(string)
		if !ok || sub == "" {
			continue
		}
		f["subdomain"] = slug + "-" + sub
	}
	out, err := json.Marshal(faces)
	if err != nil {
		return "", fmt.Errorf("preview: re-encode public_faces: %w", err)
	}
	return string(out), nil
}

// IsPreviewChild reports whether child was materialized as a branch preview
// of template (vs. an operator-created stage-chain member that merely shares
// the parent link — both use parent_workload_id). It reverses the exact
// MaterializeForBranch naming formula — name == template.Name + "/" +
// slugifyBranch(child's branch) — so a hand-named stage workload under the
// same parent is never mistaken for a preview and cascade-deleted.
func IsPreviewChild(template, child store.Workload) bool {
	if child.ParentWorkloadID != template.ID {
		return false
	}
	var cfg struct {
		Branch string `json:"branch"`
	}
	if child.SourceConfig != "" {
		_ = json.Unmarshal([]byte(child.SourceConfig), &cfg)
	}
	if cfg.Branch == "" {
		return false
	}
	return child.Name == template.Name+"/"+slugifyBranch(cfg.Branch)
}

// ListPreviewChildren returns every preview workload materialized from
// template. Used by the delete path to cascade-teardown previews so deleting
// a template does not orphan their containers, proxy routes, and rows.
func ListPreviewChildren(s Store, template store.Workload) ([]store.Workload, error) {
	children, err := s.ListChildrenByParent(template.ID)
	if err != nil {
		return nil, fmt.Errorf("preview: list children: %w", err)
	}
	out := make([]store.Workload, 0, len(children))
	for _, c := range children {
		if IsPreviewChild(template, c) {
			out = append(out, c)
		}
	}
	return out, nil
}

// MaterializeForBranch returns the existing preview workload for
// (template, branch) or creates one if none exists. The new workload
// inherits the template's source kind, trigger kind, notification
// settings, and public faces (with the branch slug prefixed onto each
// subdomain). Idempotent: a second call with the same arguments returns
// the same workload row.
func MaterializeForBranch(s Store, template store.Workload, branch string) (store.Workload, error) {
	if branch == "" {
		return store.Workload{}, fmt.Errorf("preview: branch is required")
	}

	children, err := s.ListChildrenByParent(template.ID)
	if err != nil {
		return store.Workload{}, fmt.Errorf("preview: list children: %w", err)
	}
	if existing, ok := findExistingPreview(children, branch); ok {
		return existing, nil
	}

	slug := slugifyBranch(branch)
	newCfg, err := patchSourceConfigBranch(template.SourceConfig, branch)
	if err != nil {
		return store.Workload{}, err
	}
	newFaces, err := patchPublicFacesSubdomain(template.PublicFaces, slug)
	if err != nil {
		return store.Workload{}, err
	}

	// Webhook + notification secrets are NOT copied to the preview. The
	// trigger dispatch reaches previews via the parent's trigger binding,
	// not via a per-preview inbound webhook, so the preview never needs
	// its own signing secret. Keeping these empty also stops the preview
	// from masquerading as a first-class workload in webhook routes.
	child := store.Workload{
		Kind:             template.Kind,
		Name:             template.Name + "/" + slug,
		AppID:            template.AppID,
		SourceKind:       template.SourceKind,
		SourceConfig:     newCfg,
		TriggerKind:      template.TriggerKind,
		TriggerConfig:    template.TriggerConfig,
		PublicFaces:      newFaces,
		ParentWorkloadID: template.ID,
	}
	created, err := s.CreateWorkload(child)
	if err != nil {
		return store.Workload{}, fmt.Errorf("preview: create child: %w", err)
	}
	return created, nil
}

// FindPreviewForBranch looks up an existing preview without creating
// one. Returns (Workload{}, false, nil) when no preview exists. Errors
// only on a store failure.
func FindPreviewForBranch(s Store, templateID, branch string) (store.Workload, bool, error) {
	if templateID == "" || branch == "" {
		return store.Workload{}, false, nil
	}
	children, err := s.ListChildrenByParent(templateID)
	if err != nil {
		return store.Workload{}, false, fmt.Errorf("preview: list children: %w", err)
	}
	w, ok := findExistingPreview(children, branch)
	return w, ok, nil
}
