// Package infra renders the base-infrastructure stacks (traefik, cloudflared, filebrowser) from the
// controller's config. It is PURE: templates in, file contents out — no docker, no filesystem, no IO.
// The orchestration (write the files, create the network, compose-up) lives in
// internal/stacks/infra.go (EnsureBaseStack), which owns the side effects.
//
// The templates are lifted verbatim from scripts/docker-setup.sh (the bare-metal installer, the
// historical source of truth for these stacks); bash `${VAR}` became Go template `{{.Field}}` and the
// heredoc conditionals became `{{if}}`. Image tags are PINNED here as the single source of truth — the
// web FileBrowser sync path (internal/web/handlers.go) delegates here so the pins can never diverge.
package infra

import (
	"embed"
	"fmt"
	"path/filepath"
	"strings"
	"text/template"

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

// Pinned image tags — NEVER ":latest" (a floating tag breaks reproducible golden bakes and lets the
// deployed version drift). Verified to resolve on Docker Hub before baking.
const (
	TraefikImage     = "traefik:v3.6.7"
	CloudflaredImage = "cloudflare/cloudflared:2026.6.0"
	FileBrowserImage = "gtstef/filebrowser:1.3.3-stable"
)

//go:embed templates/*.tmpl
var templateFS embed.FS

var tmpl = template.Must(template.New("infra").ParseFS(templateFS, "templates/*.tmpl"))

// FileSpec is one rendered file: its content and the mode it must be written with. The mode matters —
// the traefik .env carries the Cloudflare API token (0600), the rest are world-readable config (0644).
type FileSpec struct {
	Content string
	Mode    uint32 // os.FileMode bits (e.g. 0o600); uint32 keeps this package IO-free
}

// TraefikData is the per-customer input for the traefik stack. ACMEEmail empty → no Let's Encrypt
// (traefik serves self-signed); CFAPIToken empty → HTTP-01 instead of Cloudflare DNS-01, and no .env.
// (Wildcard proactive issuance is driven by the controller route, NOT here — see RenderControllerRoute:
// the entrypoint-level `http.tls.domains` does NOT trigger issuance in traefik v3, a router-level
// `tls.domains` does.)
type TraefikData struct {
	ACMEEmail  string
	CFAPIToken string
}

type traefikTmpl struct {
	TraefikData
	Image string
}

// CloudflaredData is the per-customer input for the cloudflared stack (just the tunnel token).
type CloudflaredData struct {
	CFTunnelToken string
}

type cloudflaredTmpl struct {
	CloudflaredData
	Image string
}

func render(name string, data any) (string, error) {
	var b strings.Builder
	if err := tmpl.ExecuteTemplate(&b, name, data); err != nil {
		return "", fmt.Errorf("render %s: %w", name, err)
	}
	return b.String(), nil
}

// RenderTraefik returns the traefik stack files: traefik.yml (static config), docker-compose.yml, and
// — only when a Cloudflare API token is set — a 0600 .env carrying CF_DNS_API_TOKEN (kept out of the
// compose file). The orchestrator additionally creates dynamic/, certs/ and an empty 0600 acme.json.
func RenderTraefik(d TraefikData) (map[string]FileSpec, error) {
	td := traefikTmpl{TraefikData: d, Image: TraefikImage}
	yml, err := render("traefik.yml.tmpl", td)
	if err != nil {
		return nil, err
	}
	compose, err := render("traefik-compose.yml.tmpl", td)
	if err != nil {
		return nil, err
	}
	files := map[string]FileSpec{
		"traefik.yml":        {Content: yml, Mode: 0o644},
		"docker-compose.yml": {Content: compose, Mode: 0o644},
	}
	if d.CFAPIToken != "" {
		env := fmt.Sprintf("# Cloudflare API token for Let's Encrypt DNS-01 challenge (Zone:DNS:Edit).\n"+
			"# Managed by felhom-controller — do not edit.\nCF_DNS_API_TOKEN=%s\n", d.CFAPIToken)
		files[".env"] = FileSpec{Content: env, Mode: 0o600}
	}
	return files, nil
}

// RenderCloudflared returns the cloudflared stack files (compose only — no bind mounts; the tunnel
// token is the entire config). Caller deploys this only when a tunnel token is configured.
func RenderCloudflared(d CloudflaredData) (map[string]FileSpec, error) {
	cd := cloudflaredTmpl{CloudflaredData: d, Image: CloudflaredImage}
	compose, err := render("cloudflared-compose.yml.tmpl", cd)
	if err != nil {
		return nil, err
	}
	return map[string]FileSpec{
		"docker-compose.yml": {Content: compose, Mode: 0o644},
	}, nil
}

// RenderFileBrowserCompose returns FileBrowser's docker-compose.yml for the given domain and storage
// volume-mount lines. Ported verbatim from internal/web/handlers.go (the single source of truth now
// lives here so the pinned image can't diverge between bring-up and the web storage-sync path).
func RenderFileBrowserCompose(domain string, storageMounts []string) string {
	storageSection := ""
	if len(storageMounts) > 0 {
		storageSection = "\n      # Storage paths (auto-generated by felhom-controller)\n" +
			strings.Join(storageMounts, "\n")
	}

	return fmt.Sprintf(`# FileBrowser Quantum — Infrastructure file manager
# Domain: files.%s
# Managed by felhom-controller. WARNING: Volume mounts are auto-generated; manual edits are overwritten.

services:
  filebrowser:
    image: %s
    container_name: filebrowser
    restart: unless-stopped
    # umask 002 so folders the customer creates here come out group-writable (2775 with the parent's
    # setgid), letting the content apps (group 1000) write into them. The gtstef/filebrowser image is a
    # single Go binary (entrypoint ./filebrowser) and does NOT honor a UMASK env (verified: -e UMASK=002
    # leaves PID1 at 0022), so we wrap the entrypoint to set the process umask before exec.
    entrypoint: ["sh", "-c", "umask 002; exec /home/filebrowser/filebrowser"]
    environment:
      - TZ=Europe/Budapest
      - FILEBROWSER_CONFIG=/home/filebrowser/config.yaml
    volumes:
      - filebrowser_data:/home/filebrowser/data
      - ./config.yaml:/home/filebrowser/config.yaml:ro%s
    networks:
      - traefik-public
    deploy:
      resources:
        limits:
          memory: 256M
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.filebrowser.rule=Host(`+"`"+`files.%s`+"`"+`)"
      - "traefik.http.routers.filebrowser.entrypoints=websecure"
      - "traefik.http.routers.filebrowser.tls=true"
      - "traefik.http.services.filebrowser.loadbalancer.server.port=80"
      - "traefik.docker.network=traefik-public"

volumes:
  filebrowser_data:

networks:
  traefik-public:
    external: true
`, domain, FileBrowserImage, storageSection, domain)
}

// RenderControllerRoute returns a traefik file-provider dynamic config routing the controller's own
// dashboard — Host(felhom.<domain>) → http://felhom-controller:8080 on websecure. This can only be
// produced POST config-pull (the v2 bootstrap.json carries no domain), which is why the controller
// wires its OWN route at bring-up instead of via a static Docker label at bootstrap time.
//
// When wildcardTLS is true (DNS-01 ACME configured = CF API token + email), this route is ALSO the
// **wildcard-issuance anchor**: its router-level `tls.domains` makes traefik proactively obtain
// `*.<domain>` + apex via Cloudflare DNS-01 at startup. Every other router (filebrowser, future apps)
// then serves that one wildcard by SNI match — no per-app certresolver labels, real cert before the
// first client connects. (Empirically, traefik v3 issues from a router-level `tls.domains` but NOT
// from the entrypoint-level `http.tls.domains` — hence this lives here, not in traefik.yml.)
// When wildcardTLS is false (no DNS-01: HTTP-01 or no ACME — wildcards need DNS-01), it emits a plain
// TLS router (traefik's self-signed default until/unless a cert exists).
func RenderControllerRoute(domain string, wildcardTLS bool) string {
	tlsBlock := "      tls: {}\n"
	if wildcardTLS {
		tlsBlock = fmt.Sprintf(`      tls:
        certResolver: letsencrypt
        domains:
          - main: "*.%s"
            sans:
              - "%s"
`, domain, domain)
	}
	return fmt.Sprintf(`# Traefik dynamic route for the felhom-controller dashboard — managed by felhom-controller.
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten.
http:
  routers:
    felhom-controller:
      rule: "Host(`+"`"+`felhom.%s`+"`"+`)"
      entryPoints:
        - websecure
      service: felhom-controller
%s  services:
    felhom-controller:
      loadBalancer:
        servers:
          - url: "http://felhom-controller:8080"
`, domain, tlsBlock)
}

// ServersTransportInsecure is the name of the traefik dynamic serversTransport that skips backend TLS
// verification. App services reference it by `<name>@file` (cross-provider: a docker-provider service
// pointing at a file-provider transport). It exists for backends that serve their OWN self-signed TLS
// on the internal docker bridge (e.g. Crafty on :8443) — there is no CA to verify a per-container
// self-signed cert against, and the hop never leaves the host's docker network. Verification stays the
// default (ON) for every other backend; only services that explicitly add the label opt out.
const ServersTransportInsecure = "insecure-skip-verify"

// RenderServersTransports returns the traefik file-provider dynamic config defining the named backend
// transports. Written to its OWN file under /etc/traefik/dynamic/ (NOT folded into the controller
// route) so the two concerns stay independent. Static and constant — no per-customer input.
func RenderServersTransports() string {
	return fmt.Sprintf(`# Traefik dynamic config — backend transports. Managed by felhom-controller.
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten.
# %s: for backends that serve their own self-signed TLS on the internal docker bridge
# (e.g. Crafty on :8443). Backend verification stays ON for all other backends.
http:
  serversTransports:
    %s:
      insecureSkipVerify: true
`, ServersTransportInsecure, ServersTransportInsecure)
}

// RenderFileBrowserConfig returns a FileBrowser Quantum config.yaml with one source per registered
// storage path (each a named sidebar entry). Empty paths → a single default /srv source. Ported
// verbatim from internal/web/handlers.go.
func RenderFileBrowserConfig(paths []settings.StoragePath) string {
	var sources string
	if len(paths) == 0 {
		sources = `    - path: "/srv"
`
	} else {
		for _, sp := range paths {
			mountName := filepath.Base(sp.Path)
			label := sp.Label
			if label == "" {
				label = mountName
			}
			sources += fmt.Sprintf("    - path: \"/srv/%s\"\n      name: %q\n      config:\n        defaultEnabled: true\n", mountName, label)
		}
	}

	return fmt.Sprintf(`# FileBrowser Quantum — managed by felhom-controller
# WARNING: This file is auto-generated. Manual edits will be overwritten.

server:
  port: 80
  baseURL: "/"
  database: "/home/filebrowser/data/database.db"
  logging:
    - levels: "info|warning|error"
  sources:
%suserDefaults:
  stickySidebar: true
  darkMode: true
  viewMode: "normal"
  showHidden: false
  dateFormat: false
  gallerySize: 3
  themeColor: "var(--blue)"
  preview:
    disableHideSidebar: false
    highQuality: true
    image: true
    video: true
    motionVideoPreview: true
    office: true
    popup: true
    autoplayMedia: true
    folder: true
  permissions:
    api: false
    admin: false
    modify: false
    share: false
    realtime: false
    delete: false
    create: false
    download: true
`, sources)
}
