// SPDX-License-Identifier: MIT

package pdfexport

import (
	"bytes"
	gocontext "context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
)

const (
	defaultWorkerTimeout = 60 * time.Second
	maxWorkerStderrBytes = 256 * 1024
)

func runWorker(input workerInput) ([]byte, userError) {
	if !setting.PDF.Enabled {
		return nil, errBadRequest("ERR_PDF_DISABLED", "PDF export is disabled.")
	}

	jobDir, err := os.MkdirTemp("", "forgejo-pdfexport-job-*")
	if err != nil {
		return nil, errInternal("ERR_PDF_INTERNAL", "Failed to export PDF.")
	}
	defer func() {
		if err := os.RemoveAll(jobDir); err != nil {
			log.Error("pdfexport: remove job dir: %v", err)
		}
	}()

	inPath := filepath.Join(jobDir, "input.json")
	outPath := filepath.Join(jobDir, "output.pdf")

	inBytes, err := json.Marshal(input)
	if err != nil {
		return nil, errInternal("ERR_PDF_INTERNAL", "Failed to export PDF.")
	}
	if err := os.WriteFile(inPath, inBytes, 0o600); err != nil {
		return nil, errInternal("ERR_PDF_INTERNAL", "Failed to export PDF.")
	}

	runtime := setting.PDF.ContainerRuntime
	if runtime == "" {
		runtime = "podman"
	}
	image := setting.PDF.WorkerImage
	if image == "" {
		image = "localhost/forgejo/pdf-worker:v0.1"
	}

	ctx, cancel := gocontext.WithTimeout(gocontext.Background(), defaultWorkerTimeout)
	defer cancel()

	args := []string{
		"run", "--rm",
		"--network=none",
		"--read-only",
		"--cap-drop=ALL",
		"--security-opt=no-new-privileges",
		"--tmpfs", "/tmp:rw,noexec,nosuid,size=1024m",
		"--volume", fmt.Sprintf("%s:/job:rw", jobDir),
		"--memory", "2g",
		"--cpus", "2",
		image,
		"node", "src/index.js",
		"--in", "/job/input.json",
		"--out", "/job/output.pdf",
	}
	if strings.Contains(runtime, "podman") {
		// Proxmox/LXC deployments often confine AppArmor and can block the default container profile load.
		args = append(args[:6], append([]string{"--security-opt=apparmor=unconfined"}, args[6:]...)...)
	}

	cmd := exec.CommandContext(ctx, runtime, args...)
	cmd.Stdout = io.Discard
	var stderr bytes.Buffer
	cmd.Stderr = &limitedWriter{W: &stderr, N: maxWorkerStderrBytes}

	if err := cmd.Run(); err != nil {
		if errors.Is(ctx.Err(), gocontext.DeadlineExceeded) {
			return nil, errInternal("ERR_PDF_TIMEOUT", "PDF export timed out.")
		}
		// Best-effort parse of worker error envelope from stderr JSONL.
		if ue, ok := parseWorkerError(stderr.String()); ok {
			return nil, ue
		}
		log.Error("pdfexport: worker failed: %v", err)
		return nil, errInternal("ERR_PDF_WORKER_FAILED", "Failed to export PDF.")
	}

	pdf, err := os.ReadFile(outPath)
	if err != nil || len(pdf) == 0 {
		return nil, errInternal("ERR_PDF_WORKER_NO_OUTPUT", "Failed to export PDF.")
	}
	return pdf, userError{}
}

type limitedWriter struct {
	W io.Writer
	N int
}

func (w *limitedWriter) Write(p []byte) (int, error) {
	if w.N <= 0 {
		return len(p), nil
	}
	if len(p) > w.N {
		p = p[:w.N]
	}
	n, err := w.W.Write(p)
	w.N -= n
	return n, err
}

func parseWorkerError(stderr string) (userError, bool) {
	type workerErr struct {
		ErrorID string `json:"error_id"`
		Message string `json:"message"`
	}

	lines := strings.Split(stderr, "\n")
	for i := len(lines) - 1; i >= 0; i-- {
		line := strings.TrimSpace(lines[i])
		if line == "" {
			continue
		}
		var we workerErr
		if err := json.Unmarshal([]byte(line), &we); err != nil {
			continue
		}
		if we.ErrorID == "" || we.Message == "" {
			continue
		}
		return errInternal(we.ErrorID, we.Message), true
	}
	return userError{}, false
}
