package commands

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// buildSystemdRunShell runs `bash -c <script>` as the OS user inside the
// user's transient slice (reuses buildSystemdRunCmd's PATH pinning + slice).
func buildSystemdRunShell(ctx context.Context, osUser, script string) *exec.Cmd {
	return buildSystemdRunCmd(ctx, osUser, "bash", "-c", script)
}

// buildSystemdRunCmdEnv is buildSystemdRunCmd plus extra --setenv=K=V pairs,
// spliced in before the target binary so a secret (e.g. an admin password
// for `artisan tinker`) is passed via the transient unit's environment
// rather than argv — argv is world-readable via /proc without hidepid.
func buildSystemdRunCmdEnv(ctx context.Context, osUser string, setenv []string, args ...string) *exec.Cmd {
	cmd := buildSystemdRunCmd(ctx, osUser, args...)
	// cmd.Args = [systemd-run --quiet --uid=… … --collect <binary> <args…>].
	// Insert the --setenv flags right after "--collect" so they apply to the
	// unit. Find --collect; fall back to inserting after the first arg.
	insertAt := 1
	for i, a := range cmd.Args {
		if a == "--collect" {
			insertAt = i + 1
			break
		}
	}
	flags := make([]string, 0, len(setenv))
	for _, kv := range setenv {
		flags = append(flags, "--setenv="+kv)
	}
	cmd.Args = append(cmd.Args[:insertAt], append(flags, cmd.Args[insertAt:]...)...)
	return cmd
}

// invoiceShelfSnippetPath mirrors flarumSnippetPath — a per-install nginx
// include keyed by the subdir (or "root" for a docroot install).
func invoiceShelfSnippetPath(domain, subdir string) string {
	slug := subdir
	if slug == "" {
		slug = "root"
	}
	return filepath.Join(nginxJabaliDir, domain, "invoiceshelf-"+slug+".conf")
}

// invoiceShelfNginxConf hardens the flattened Laravel webroot. After the
// public/ flatten, the app's sensitive dirs (vendor/, storage/, config/,
// bootstrap/, database/, app/, resources/, routes/, lang/) and files (.env,
// artisan, composer.*) sit IN the webroot, so they are denied with `=`/`^~`
// locations that beat the vhost's `~ \.php$` (nginx precedence: = > ^~ >
// ~). Subdir installs also get a Laravel front controller since the generic
// vhost's try_files targets /index.php at the docroot, not /<sub>/index.php.
func invoiceShelfNginxConf(osUser, subdir string) string {
	p := ""
	if subdir != "" {
		p = "/" + subdir
	}
	var b strings.Builder
	b.WriteString("# Auto-generated by jabali-agent. Do not edit.\n")
	b.WriteString("# InvoiceShelf (flattened Laravel) sensitive-path hardening.\n")
	for _, f := range []string{"/.env", "/artisan", "/composer.json", "/composer.lock"} {
		fmt.Fprintf(&b, "location = %s%s { deny all; return 404; }\n", p, f)
	}
	for _, d := range []string{"/vendor/", "/storage/", "/bootstrap/", "/config/", "/database/", "/app/", "/resources/", "/routes/", "/lang/", "/tests/", "/.git"} {
		fmt.Fprintf(&b, "location ^~ %s%s { deny all; return 404; }\n", p, d)
	}
	if subdir != "" {
		fmt.Fprintf(&b, "location ^~ %s/ {\n", p)
		b.WriteString("    location ~ \\.php$ {\n")
		fmt.Fprintf(&b, "        fastcgi_pass unix:/run/php/jabali-%s/fpm.sock;\n", osUser)
		b.WriteString("        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n")
		b.WriteString("        include fastcgi_params;\n")
		b.WriteString("    }\n")
		fmt.Fprintf(&b, "    try_files $uri $uri/ %s/index.php?$query_string;\n", p)
		b.WriteString("}\n")
	}
	return b.String()
}

func writeInvoiceShelfNginx(ctx context.Context, domain, osUser, subdir string) error {
	if !domainRegex.MatchString(domain) {
		return fmt.Errorf("invalid domain %q", domain)
	}
	if subdir != "" && !subdirSlugRE.MatchString(subdir) {
		return fmt.Errorf("invalid subdir slug %q", subdir)
	}
	if !osUserSafeRE.MatchString(osUser) {
		return fmt.Errorf("invalid os_user %q", osUser)
	}
	domainDir := filepath.Join(nginxJabaliDir, domain)
	if err := os.MkdirAll(domainDir, 0o755); err != nil {
		return fmt.Errorf("mkdir %s: %w", domainDir, err)
	}
	dest := invoiceShelfSnippetPath(domain, subdir)
	if err := os.WriteFile(dest, []byte(invoiceShelfNginxConf(osUser, subdir)), 0o644); err != nil {
		return fmt.Errorf("write %s: %w", dest, err)
	}
	return reloadNginxAfterSnippet(ctx, dest)
}

func removeInvoiceShelfNginx(ctx context.Context, domain, subdir string) error {
	if !domainRegex.MatchString(domain) {
		return fmt.Errorf("invalid domain %q", domain)
	}
	// Validate subdir before it is glued into the snippet FILENAME: an
	// unvalidated "../.." here would let filepath.Join escape the domain
	// dir and os.Remove (root) an arbitrary .conf. The write path already
	// checks this; the remove path must too (defense-in-depth, and the
	// deleter passes the subdir straight from the request).
	if subdir != "" && !subdirSlugRE.MatchString(subdir) {
		return fmt.Errorf("invalid subdir slug %q", subdir)
	}
	dest := invoiceShelfSnippetPath(domain, subdir)
	if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("remove %s: %w", dest, err)
	}
	return reloadNginxAfterSnippet(ctx, dest)
}
