// Package dockerfile implements the "dockerfile" source: a git-repo-backed
// deployable that builds a Docker image from a user-supplied Dockerfile
// and runs one container. This is the "self-hosted Vercel" Source —
// users point at a Git repo containing a Dockerfile and Tinyforge
// handles clone → build → run → proxy in one shot, with no external CI
// pipeline.
//
// Architecturally the plugin sits between `static` (clones a Git repo,
// builds an image, runs one container) and `image` (richer runtime
// shape: ports, healthcheck, env, volumes). The deploy pipeline mirrors
// static — same git-fetch, same image-tag/container-name shape, same
// container-row state persistence — but the build step uses the
// operator's Dockerfile instead of generating one.
//
// The full pipeline is implemented inline in this package
// (deploy.go / teardown.go / reconcile.go) so a new dockerfile source
// kind is usable immediately on init() — no separate registration step
// in the deployer.
package dockerfile

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

	"github.com/alexei/tinyforge/internal/workload/plugin"
)

// Config is the per-workload source config blob. Mirrors the shape of
// the static plugin's Config so the UI wizard can largely reuse the
// existing Git-discovery + branch-picker + repo-picker components.
//
// Build-side fields:
//
//   - DockerfilePath: path to the Dockerfile *within the context*
//     directory. Defaults to "Dockerfile". Use e.g. "docker/Dockerfile"
//     when the operator's repo keeps Dockerfiles in a subfolder.
//   - ContextPath: subfolder of the cloned repo to use as the build
//     context. Defaults to "" (repo root). Use e.g. "./api" when the
//     repo's Dockerfile lives next to a backend service in a monorepo.
//
// Runtime-side fields:
//
//   - Port: container port the workload listens on. Required.
//   - Healthcheck: optional curl-style probe; empty disables.
//
// Env vars and volume mounts are handled out-of-band via the
// workload_env and workload_volumes tables, mirroring the image source.
type Config struct {
	Provider       string `json:"provider"`         // "gitea" | "github" | "gitlab"; "" = autodetect
	BaseURL        string `json:"base_url"`         // e.g. https://git.example.com
	RepoOwner      string `json:"repo_owner"`
	RepoName       string `json:"repo_name"`
	Branch         string `json:"branch"`
	ContextPath    string `json:"context_path"`     // path within repo (root by default)
	DockerfilePath string `json:"dockerfile_path"`  // relative to context_path; "Dockerfile" by default
	AccessToken    string `json:"access_token"`     // encrypted; optional for public repos

	Port        int    `json:"port"`
	Healthcheck string `json:"healthcheck,omitempty"`

	// ReportCommitStatus, when true, pushes the deploy outcome back to the
	// git provider as a commit status (pending/success/failure) on the
	// built SHA. Best-effort — a reporting failure never fails a deploy.
	ReportCommitStatus bool `json:"report_commit_status"`

	// DeployStrategy selects how a redeploy cuts over. "" (default) and
	// "recreate" stop the old container before starting the new one (a brief
	// downtime window). "blue-green" starts the new build alongside the old,
	// gates it, swaps the proxy route in place, then reaps the old —
	// zero-downtime under NPM. Validated via plugin.ValidateStrategy.
	DeployStrategy string `json:"deploy_strategy,omitempty"`
}

// effectiveStrategy resolves the configured strategy for the dockerfile
// source. Empty maps to recreate — the source's historical behavior — so
// existing workloads are unchanged.
func effectiveStrategy(cfg Config) string {
	if cfg.DeployStrategy == "" {
		return plugin.StrategyRecreate
	}
	return cfg.DeployStrategy
}

type source struct{}

// Eager registration — the deploy pipeline lives entirely inside this
// package, so the kind is usable as soon as init() fires.
func init() { plugin.RegisterSource(&source{}) }

func (*source) Kind() string { return "dockerfile" }

func (*source) SchemaSample() any {
	return Config{
		Provider:       "gitea",
		BaseURL:        "https://git.example.com",
		RepoOwner:      "owner",
		RepoName:       "myservice",
		Branch:         "main",
		ContextPath:    "",
		DockerfilePath: "Dockerfile",
		Port:           8080,
	}
}

// Validate rejects obviously-malformed configs before the deploy
// pipeline materializes a temp dir, downloads a repo, and burns
// minutes of build time on input that was never going to work.
func (*source) Validate(cfg json.RawMessage) error {
	var c Config
	if len(cfg) == 0 {
		return fmt.Errorf("dockerfile source: config is required")
	}
	if err := json.Unmarshal(cfg, &c); err != nil {
		return fmt.Errorf("dockerfile source: invalid json: %w", err)
	}
	if strings.TrimSpace(c.RepoOwner) == "" || strings.TrimSpace(c.RepoName) == "" {
		return fmt.Errorf("dockerfile source: repo_owner and repo_name are required")
	}
	if c.Port <= 0 || c.Port > 65535 {
		return fmt.Errorf("dockerfile source: port must be between 1 and 65535 (got %d)", c.Port)
	}
	// Defense in depth: a leading "/" or any ".." segment in
	// DockerfilePath / ContextPath would escape the build context. The
	// plugin's deploy() does its own normalization too; rejecting here
	// gives the operator a clear error at save-time instead of a
	// confusing "no such file" mid-build.
	for _, p := range []string{c.DockerfilePath, c.ContextPath} {
		if p == "" {
			continue
		}
		if strings.HasPrefix(p, "/") {
			return fmt.Errorf("dockerfile source: %q must be relative", p)
		}
		if strings.Contains(p, "..") {
			return fmt.Errorf("dockerfile source: %q must not contain '..'", p)
		}
	}
	if err := plugin.ValidateStrategy(c.DeployStrategy, true); err != nil {
		return fmt.Errorf("dockerfile source: %w", err)
	}
	return nil
}

func (*source) Deploy(ctx context.Context, deps plugin.Deps, w plugin.Workload, intent plugin.DeploymentIntent) error {
	return deploy(ctx, deps, w, intent)
}

func (*source) Teardown(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
	return teardown(ctx, deps, w)
}

func (*source) Reconcile(ctx context.Context, deps plugin.Deps, w plugin.Workload) error {
	return reconcile(ctx, deps, w)
}
