package handlers

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"

	"github.com/go-chi/chi/v5"
	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/models"
)

type ArtifactHandler struct {
	db           *xorm.Engine
	artifactRoot string
}

func NewArtifactHandler(db *xorm.Engine, artifactRoot string) *ArtifactHandler {
	return &ArtifactHandler{db: db, artifactRoot: artifactRoot}
}

// ListArtifacts returns all artifacts for a pipeline run.
func (h *ArtifactHandler) List(w http.ResponseWriter, r *http.Request) {
	repoID, runID, ok := h.resolveRunIDs(w, r)
	if !ok {
		return
	}
	var artifacts []models.Artifact
	if err := h.db.Where("run_id = ? AND repo_id = ?", runID, repoID).Find(&artifacts); err != nil {
		jsonError(w, "could not list artifacts", http.StatusInternalServerError)
		return
	}
	if artifacts == nil {
		artifacts = []models.Artifact{}
	}
	jsonOK(w, artifacts)
}

// Upload accepts a multipart file upload and stores it as an artifact.
// Callers must provide a valid Bearer access token with write scope (runner auth).
func (h *ArtifactHandler) Upload(w http.ResponseWriter, r *http.Request) {
	repoID, runID, ok := h.resolveRunIDs(w, r)
	if !ok {
		return
	}

	r.Body = http.MaxBytesReader(w, r.Body, 512<<20) // 512 MB max
	if err := r.ParseMultipartForm(32 << 20); err != nil {
		jsonError(w, "multipart parse failed", http.StatusBadRequest)
		return
	}
	file, header, err := r.FormFile("file")
	if err != nil {
		jsonError(w, "file field is required", http.StatusBadRequest)
		return
	}
	defer file.Close()

	name := r.FormValue("name")
	if name == "" {
		name = header.Filename
	}

	// Sanitize name: no path separators.
	for _, c := range []byte(name) {
		if c == '/' || c == '\\' || c == 0 {
			jsonError(w, "artifact name must not contain path separators", http.StatusBadRequest)
			return
		}
	}

	dir := filepath.Join(h.artifactRoot, fmt.Sprintf("%d", runID))
	if err := os.MkdirAll(dir, 0755); err != nil {
		jsonError(w, "could not create storage directory", http.StatusInternalServerError)
		return
	}

	storagePath := filepath.Join(dir, name)
	dst, err := os.Create(storagePath)
	if err != nil {
		jsonError(w, "could not create file", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	size, err := io.Copy(dst, file)
	if err != nil {
		jsonError(w, "could not write file", http.StatusInternalServerError)
		return
	}

	ct := header.Header.Get("Content-Type")
	if ct == "" {
		ct = "application/octet-stream"
	}

	// Store path relative to artifactRoot for portability.
	relPath := fmt.Sprintf("%d/%s", runID, name)

	artifact := &models.Artifact{
		RunID:       runID,
		RepoID:      repoID,
		Name:        name,
		StoragePath: relPath,
		Size:        size,
		ContentType: ct,
	}
	if _, err := h.db.Insert(artifact); err != nil {
		jsonError(w, "could not record artifact", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusCreated)
	jsonOK(w, artifact)
}

// Download streams the artifact file to the client.
func (h *ArtifactHandler) Download(w http.ResponseWriter, r *http.Request) {
	artifactID, err := strconv.ParseInt(chi.URLParam(r, "artifactID"), 10, 64)
	if err != nil {
		jsonError(w, "invalid artifact ID", http.StatusBadRequest)
		return
	}
	var artifact models.Artifact
	if found, _ := h.db.ID(artifactID).Get(&artifact); !found {
		jsonError(w, "artifact not found", http.StatusNotFound)
		return
	}

	fullPath := filepath.Join(h.artifactRoot, filepath.FromSlash(artifact.StoragePath))
	// Ensure the resolved path stays within artifactRoot (traversal guard).
	if !isUnder(h.artifactRoot, fullPath) {
		jsonError(w, "forbidden", http.StatusForbidden)
		return
	}

	f, err := os.Open(fullPath)
	if err != nil {
		jsonError(w, "artifact file not found", http.StatusNotFound)
		return
	}
	defer f.Close()

	ct := artifact.ContentType
	if ct == "" {
		ct = "application/octet-stream"
	}
	w.Header().Set("Content-Type", ct)
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, artifact.Name))
	w.Header().Set("Content-Length", strconv.FormatInt(artifact.Size, 10))
	io.Copy(w, f) //nolint:errcheck
}

func (h *ArtifactHandler) resolveRunIDs(w http.ResponseWriter, r *http.Request) (repoID, runID int64, ok bool) {
	owner := chi.URLParam(r, "owner")
	repoName := chi.URLParam(r, "repo")
	var u models.User
	if found, _ := h.db.Where("username = ?", owner).Get(&u); !found {
		jsonError(w, "repository not found", http.StatusNotFound)
		return 0, 0, false
	}
	var repo models.Repository
	if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found {
		jsonError(w, "repository not found", http.StatusNotFound)
		return 0, 0, false
	}
	runID, err := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64)
	if err != nil {
		jsonError(w, "invalid run ID", http.StatusBadRequest)
		return 0, 0, false
	}
	return repo.ID, runID, true
}

func isUnder(root, path string) bool {
	root = filepath.Clean(root)
	path = filepath.Clean(path)
	if len(path) <= len(root) {
		return false
	}
	return path[:len(root)] == root && path[len(root)] == filepath.Separator
}
