package main

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gin-gonic/gin"
)

// renameBody mirrors the Node prototype's wire contract — a single
// `newName` field carrying the bare basename (no slashes).
type renameBody struct {
	NewName string `json:"newName"`
}

// ppPhoto is the partial PhotoPrism photo shape we need to find the
// primary file's on-disk location. Anything we don't read stays
// unspecified so version drift across PhotoPrism builds doesn't break
// JSON unmarshalling.
type ppPhoto struct {
	Files []ppFile `json:"Files"`
}

type ppFile struct {
	Name    string `json:"Name"`
	Root    string `json:"Root"`
	Primary bool   `json:"Primary"`
}

func primaryFileOf(p *ppPhoto) (ppFile, bool) {
	if p == nil {
		return ppFile{}, false
	}
	for _, f := range p.Files {
		if f.Primary {
			return f, true
		}
	}
	if len(p.Files) > 0 {
		return p.Files[0], true
	}
	return ppFile{}, false
}

func handleRename(cfg *Config, pp *ppClient) gin.HandlerFunc {
	return func(c *gin.Context) {
		token := ctxToken(c)
		photoUID := c.Param("uid")

		var body renameBody
		if err := c.ShouldBindJSON(&body); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
			return
		}
		newName, ok := sanitizeFilename(body.NewName)
		if !ok {
			c.JSON(http.StatusBadRequest, gin.H{"error": "newName must be a plain filename"})
			return
		}

		// Fetch the photo so we can resolve Files[0].Root + Name into a
		// concrete on-disk path. PhotoPrism has no "file by UID" endpoint
		// in this build, so the single-photo lookup is the cheapest path.
		resp, err := pp.call(c.Request.Context(), http.MethodGet, "/api/v1/photos/"+photoUID, token, nil)
		if err != nil {
			c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
			return
		}
		if !resp.OK {
			c.JSON(resp.Status, gin.H{"error": "photo not found"})
			return
		}
		var photo ppPhoto
		if err := json.Unmarshal(resp.Body, &photo); err != nil {
			c.JSON(http.StatusBadGateway, gin.H{"error": "decode photo"})
			return
		}
		file, ok := primaryFileOf(&photo)
		if !ok {
			c.JSON(http.StatusNotFound, gin.H{"error": "no files on this photo"})
			return
		}

		root := strings.TrimPrefix(file.Root, "/")
		if root == "" || root == "/" {
			root = ""
		}
		relPath := filepath.Join(root, file.Name)
		oldAbs := filepath.Join(cfg.OriginalsRoot, relPath)

		if !ensureWithinOriginals(cfg.OriginalsRoot, oldAbs) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "path escapes originals root"})
			return
		}
		st, err := os.Stat(oldAbs)
		if err != nil || !st.Mode().IsRegular() {
			c.JSON(http.StatusNotFound, gin.H{"error": "file missing on disk"})
			return
		}
		newAbs := filepath.Join(filepath.Dir(oldAbs), newName)
		if !ensureWithinOriginals(cfg.OriginalsRoot, newAbs) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "new path escapes originals root"})
			return
		}
		if _, err := os.Stat(newAbs); err == nil {
			c.JSON(http.StatusConflict, gin.H{"error": "target filename already exists"})
			return
		} else if !errors.Is(err, os.ErrNotExist) {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		newRel := filepath.Join(root, newName)
		slog.Info("rename", "from", relPath, "to", newRel)
		if err := os.Rename(oldAbs, newAbs); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		// Trigger reindex on the parent so PhotoPrism picks up the new
		// filename and drops the orphan row for the old name. Best-effort.
		reindexPath := "/"
		if root != "" {
			reindexPath = "/" + root
		}
		if err := pp.reindex(c.Request.Context(), token, reindexPath); err != nil {
			slog.Warn("rename reindex failed", "err", err)
		}

		c.JSON(http.StatusOK, gin.H{
			"ok":         true,
			"oldName":    file.Name,
			"newName":    newName,
			"oldRelPath": relPath,
			"newRelPath": newRel,
		})
	}
}
