package uploads

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"path"
	"strings"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/pkg/httpresponse"
	"github.com/TaruLab/keluargamart-backend/pkg/logger"
	"github.com/TaruLab/keluargamart-backend/pkg/middleware"
	"github.com/TaruLab/keluargamart-backend/pkg/storage"
	"github.com/go-chi/chi/v5"
)

// maxUploadBytes is the hard cap on an uploaded file body (5 MB).
const maxUploadBytes int64 = 5 * 1024 * 1024

// ProofPrefix marks stored keys that hold a payment proof. Payment proofs are
// private financial documents (cheque images, signatures, account details), so
// the marker lets Serve classify a key as private with no database round trip.
// Keys written before this marker existed are legacy single-segment keys; they
// are classified by the injected ProofAuthorizer, which is authoritative.
const ProofPrefix = "proof-"

// Sentinel errors returned by SaveUpload, mapped to HTTP responses by callers.
var (
	ErrInvalidForm     = errors.New("request body too large or invalid multipart form")
	ErrMissingFile     = errors.New("missing 'file' multipart field")
	ErrUnsupportedType = errors.New("unsupported file type; allowed: jpeg, png, webp, gif")
)

// allowedTypes are the detected content types we accept for upload.
var allowedTypes = map[string]string{
	"image/jpeg": ".jpg",
	"image/png":  ".png",
	"image/webp": ".webp",
	"image/gif":  ".gif",
}

// Handler serves and stores uploaded binary objects (images) via the
// storage.Store seam rooted at dir.
type Handler struct {
	store  storage.Store
	dir    string
	proofs ProofAuthorizer
	// authenticate resolves a bearer token to (user, token) for the
	// query-token alternative on <img> requests. Nil = query-token path off.
	authenticate func(ctx context.Context, bearer string) (*auth.User, error)
}

// NewHandler builds an uploads Handler bound to the given store and root dir.
func NewHandler(store storage.Store, dir string) *Handler {
	return &Handler{store: store, dir: dir}
}

// WithAuthenticator wires the token authenticator used by the ?token=
// alternative on object GETs (000151): an <img> tag cannot send an
// Authorization header, so private proof photos were unreachable from the
// ERP detail view ("object not found"). The authenticator is the SAME
// auth.Service.Authenticate used by the API middleware — fail-closed when
// nil, and proof authorization still runs afterwards, unchanged.
func (h *Handler) WithAuthenticator(fn func(ctx context.Context, bearer string) (*auth.User, error)) *Handler {
	if fn != nil {
		h.authenticate = fn
	}
	return h
}

// ProofAccess is the authorization verdict for one stored key and one caller.
type ProofAccess struct {
	// Private reports that the key holds a private document (a payment proof)
	// rather than a public image.
	Private bool
	// Allowed reports whether this caller may read a private document. It is
	// only consulted when Private is true.
	Allowed bool
}

// ProofAuthorizer classifies a stored key and decides whether the caller may
// read it. u is the authenticated user, or nil for an anonymous request.
//
// It is a narrow function seam rather than a direct dependency on the finance
// module: internal/uploads must not import internal/finance (finance imports
// uploads for the shared save path), so the real, payments-table-backed
// implementation is injected at the wiring site in cmd/server. Because the
// authorizer is consulted for EVERY key, proofs stored before ProofPrefix
// existed stay authorization-gated instead of public.
type ProofAuthorizer func(ctx context.Context, key string, u *auth.User) (ProofAccess, error)

// WithProofAuthorizer wires the payment-proof authorization seam used by Serve.
// Until it is wired, Serve cannot prove any key is public and therefore serves
// nothing (fail closed).
func (h *Handler) WithProofAuthorizer(fn ProofAuthorizer) *Handler {
	h.proofs = fn
	return h
}

// Upload accepts a multipart form field `file`, validates size (5 MB) and
// detected content type, stores it under an opaque key, and returns the
// public URL. Requires an authenticated caller (enforced by route wiring).
func (h *Handler) Upload(w http.ResponseWriter, r *http.Request) {
	reqID := middleware.RequestIDFrom(r.Context())
	log := logger.FromContext(r.Context())

	url, err := SaveUpload(r.Context(), h.store, r)
	if err != nil {
		switch {
		case errors.Is(err, ErrInvalidForm), errors.Is(err, ErrMissingFile), errors.Is(err, ErrUnsupportedType):
			httpresponse.Error(w, http.StatusBadRequest, reqID, httpresponse.CodeValidation, err.Error(), nil)
		default:
			httpresponse.Internal(w, reqID, log, err)
		}
		return
	}

	httpresponse.OK(w, http.StatusCreated, reqID, map[string]string{
		"url": url,
	})
}

// SaveUpload stores a multipart form field `file` from r via store, applying
// the same size (5 MB) and detected content-type limits as the generic
// uploads endpoint. It returns the public "/uploads/<key>" URL. It is shared
// by POST /api/v1/uploads and feature endpoints that accept image
// attachments so every upload path enforces identical limits and key
// generation.
func SaveUpload(ctx context.Context, store storage.Store, r *http.Request) (string, error) {
	return save(ctx, store, r, "")
}

// SaveFileHeader stores an already-parsed multipart file header through the
// same pipeline as save() — same size cap, same content-type sniffing, same
// opaque key generation (with prefix). For callers that receive the file
// inside their OWN request alongside other fields (e.g. the POD photo riding
// a DO completion), so they do not have to re-wrap bytes into a synthetic
// multipart request just to reuse the storage rules.
func SaveFileHeader(ctx context.Context, store storage.Store, header *multipart.FileHeader, prefix string) (string, error) {
	f, err := header.Open()
	if err != nil {
		return "", ErrMissingFile
	}
	defer f.Close()
	return saveFromReader(ctx, store, f, prefix)
}

// SaveProofUpload stores a payment proof with the identical limits and
// server-generated opaque key as SaveUpload, but marks the key with
// ProofPrefix. The marker is what lets the read path tell a private financial
// document from a public product image without consulting the payments table;
// ownership is still resolved authoritatively, so the marker is a cheap
// short-circuit and never the authorization itself.
func SaveProofUpload(ctx context.Context, store storage.Store, r *http.Request) (string, error) {
	return save(ctx, store, r, ProofPrefix)
}

func save(ctx context.Context, store storage.Store, r *http.Request, prefix string) (string, error) {
	r.Body = http.MaxBytesReader(nil, r.Body, maxUploadBytes)
	if err := r.ParseMultipartForm(maxUploadBytes); err != nil {
		return "", ErrInvalidForm
	}

	file, _, err := r.FormFile("file")
	if err != nil {
		return "", ErrMissingFile
	}
	defer file.Close()

	return saveFromReader(ctx, store, file, prefix)
}

// saveFromReader is the storage core both request-driven and header-driven
// callers share: sniff the first 512 bytes against the type allowlist, mint
// an opaque key, write the object.
func saveFromReader(ctx context.Context, store storage.Store, file multipart.File, prefix string) (string, error) {
	// Read the first 512 bytes to sniff the content type; cap the reader so a
	// malformed/missing Content-Type header cannot be abused.
	buf := make([]byte, 512)
	n, readErr := io.ReadFull(file, buf)
	if readErr != nil && readErr != io.ErrUnexpectedEOF {
		return "", readErr
	}
	detected := http.DetectContentType(buf[:n])
	ext, ok := allowedTypes[detected]
	if !ok {
		return "", ErrUnsupportedType
	}

	key, err := newKey(prefix, ext)
	if err != nil {
		return "", err
	}

	// Reconstruct a reader over the sniffed prefix plus the remaining body.
	body := io.MultiReader(strings.NewReader(string(buf[:n])), file)
	if err := store.Put(ctx, key, body); err != nil {
		return "", err
	}
	return "/uploads/" + key, nil
}

// IsProofKey reports whether a stored key carries the payment-proof marker, so
// the read path can classify it as private without a database round trip.
func IsProofKey(key string) bool {
	return strings.HasPrefix(key, ProofPrefix)
}

// Serve streams a stored object by its {key} path param. Public images
// (product photos and the like) stay anonymous and immutably cacheable;
// payment proofs are private financial documents and are served only to the
// owner or to staff the injected authorizer allows, with private cache
// headers. The caller (store) is responsible for closing the returned body.
func (h *Handler) Serve(w http.ResponseWriter, r *http.Request) {
	reqID := middleware.RequestIDFrom(r.Context())
	log := logger.FromContext(r.Context())
	key := chi.URLParam(r, "key")
	if key == "" {
		httpresponse.Error(w, http.StatusBadRequest, reqID, httpresponse.CodeValidation,
			"missing key", nil)
		return
	}
	// Reject traversal with the same rule pkg/storage applies, but before the
	// ownership lookup so a bogus key cannot cost a database round trip.
	if hasTraversal(key) {
		notFound(w, reqID)
		return
	}

	// Query-token alternative (000151): an <img> tag cannot send an
	// Authorization header, so the ERP detail view could never render a
	// private proof photo ("object not found"). When the caller supplies
	// ?token=<plain bearer> and no header, resolve it through the SAME
	// Authenticate the middleware uses, then carry the user on the context —
	// proof authorization below is unchanged and still fail-closed. A tampered
	// or expired token simply resolves to no user and falls through to the
	// unauthenticated path.
	if auth.UserFrom(r.Context()) == nil && h.authenticate != nil {
		if tok := strings.TrimSpace(r.URL.Query().Get("token")); tok != "" {
			// Authenticate expects the PLAIN token (middleware strips the
			// "Bearer " prefix before calling it) — so pass the raw value.
			if u, err := h.authenticate(r.Context(), tok); err == nil && u != nil {
				r = r.WithContext(auth.ToContext(r.Context(), u, u.ID))
			}
		}
	}

	// Authorize BEFORE touching the store: a private proof must not be opened
	// (or timed) for a caller who may not have it. An unavailable authorizer
	// fails closed — nothing is served.
	access, err := h.proofAccess(r.Context(), key, auth.UserFrom(r.Context()))
	if err != nil {
		httpresponse.Internal(w, reqID, log, err)
		return
	}
	if access.Private && !access.Allowed {
		// Same 404 as a missing object, so an unauthorized reader cannot probe
		// which private keys exist. Unauthenticated callers land here too.
		notFound(w, reqID)
		return
	}

	rc, err := h.store.Get(r.Context(), key)
	if err != nil {
		if errors.Is(err, storage.ErrNotFound) || errors.Is(err, storage.ErrInvalidKey) {
			notFound(w, reqID)
			return
		}
		httpresponse.Internal(w, reqID, log, err)
		return
	}
	defer rc.Close()

	ctype := mime.TypeByExtension(path.Ext(key))
	if ctype == "" {
		ctype = "application/octet-stream"
	}
	w.Header().Set("Content-Type", ctype)
	if access.Private {
		// A customer's financial document must never be retained by a shared
		// cache or the browser back/forward cache.
		w.Header().Set("Cache-Control", "private, no-store")
	} else {
		w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
	}
	w.WriteHeader(http.StatusOK)
	io.Copy(w, rc)
}

// proofAccess resolves the read policy for key. With no authorizer wired the
// handler cannot prove a key is public, so every key is reported private and
// unreadable.
func (h *Handler) proofAccess(ctx context.Context, key string, u *auth.User) (ProofAccess, error) {
	if h.proofs == nil {
		return ProofAccess{Private: true}, nil
	}
	access, err := h.proofs(ctx, key, u)
	if err != nil {
		return ProofAccess{}, err
	}
	// A proof-prefixed key is private on its face, even if no payment row
	// resolves for it (deleted payment, no linked customer): fail closed.
	if IsProofKey(key) {
		access.Private = true
	}
	return access, nil
}

func notFound(w http.ResponseWriter, reqID string) {
	httpresponse.Error(w, http.StatusNotFound, reqID, httpresponse.CodeNotFound,
		"object not found", nil)
}

// hasTraversal mirrors the pkg/storage rule so the handler rejects a traversing
// key without spending an ownership lookup on it.
func hasTraversal(key string) bool {
	norm := strings.ReplaceAll(key, "\\", "/")
	for _, seg := range strings.Split(norm, "/") {
		if seg == ".." {
			return true
		}
	}
	return false
}

// newKey generates a 32-hex-char opaque key with the supplied extension,
// optionally under a namespace prefix.
func newKey(prefix, ext string) (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return prefix + hex.EncodeToString(b) + ext, nil
}
