package handlers

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"warpbox.dev/backend/libs/helpers"
	"warpbox.dev/backend/libs/services"
	"warpbox.dev/backend/libs/web"
)

type downloadPageData struct {
	Box           boxView
	Files         []fileView
	ZipURL        string
	Locked        bool
	Obfuscated    bool
	CanPreview    bool
	DownloadCount int
	MaxDownloads  int
	ExpiresLabel  string
}

type boxView struct {
	ID string
}

type fileView struct {
	ID           string
	Name         string
	Size         string
	ContentType  string
	PreviewKind  string
	URL          string
	DownloadURL  string
	ThumbnailURL string
}

type previewPageData struct {
	Box         boxView
	File        fileView
	Locked      bool
	DownloadURL string
}

func (a *App) DownloadPage(w http.ResponseWriter, r *http.Request) {
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	if err := a.uploadService.CanDownload(box); err != nil {
		a.renderPage(w, r, http.StatusForbidden, "download.html", web.PageData{
			Title:       "Download unavailable",
			Description: "This Warpbox link is no longer available.",
			Data: downloadPageData{
				Box:          boxView{ID: box.ID},
				ExpiresLabel: err.Error(),
			},
		})
		return
	}
	locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box)

	files := make([]fileView, 0, len(box.Files))
	if !(locked && box.Obfuscate) {
		for _, file := range box.Files {
			files = append(files, a.fileView(box, file))
		}
	}

	expiresLabel := box.ExpiresAt.Format("Jan 2, 2006 15:04 MST")
	title := "Shared files on Warpbox"
	description := fmt.Sprintf("%d file%s shared via Warpbox · expires %s", len(box.Files), plural(len(box.Files)), expiresLabel)
	if locked && box.Obfuscate {
		title = "Protected Warpbox link"
		description = "This shared box is password protected."
	}

	a.renderPage(w, r, http.StatusOK, "download.html", web.PageData{
		Title:       title,
		Description: description,
		ImageURL:    absoluteURL(r, fmt.Sprintf("/d/%s/og-image.jpg", box.ID)),
		Data: downloadPageData{
			Box:           boxView{ID: box.ID},
			Files:         files,
			ZipURL:        fmt.Sprintf("/d/%s/zip", box.ID),
			Locked:        locked,
			Obfuscated:    box.Obfuscate,
			DownloadCount: box.DownloadCount,
			MaxDownloads:  box.MaxDownloads,
			ExpiresLabel:  expiresLabel,
		},
	})
}

func plural(n int) string {
	if n == 1 {
		return ""
	}
	return "s"
}

func (a *App) DownloadFile(w http.ResponseWriter, r *http.Request) {
	box, file, ok := a.loadFileForRequest(w, r)
	if !ok {
		return
	}

	locked := a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box)
	view := a.fileView(box, file)
	title := file.Name
	description := fmt.Sprintf("%s shared via Warpbox", helpers.FormatBytes(file.Size))
	imageURL := absoluteURL(r, view.ThumbnailURL)
	if locked && box.Obfuscate {
		title = "Protected Warpbox file"
		description = "This shared file is password protected."
		imageURL = absoluteURL(r, "/static/img/file-placeholder.webp")
	}

	a.renderPage(w, r, http.StatusOK, "preview.html", web.PageData{
		Title:       title,
		Description: description,
		ImageURL:    imageURL,
		Data: previewPageData{
			Box:         boxView{ID: box.ID},
			File:        view,
			Locked:      locked,
			DownloadURL: view.DownloadURL,
		},
	})
}

func (a *App) DownloadFileContent(w http.ResponseWriter, r *http.Request) {
	box, file, ok := a.loadFileForRequest(w, r)
	if !ok {
		return
	}
	if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) {
		http.Error(w, "password required", http.StatusUnauthorized)
		return
	}

	a.serveFileContent(w, r, box, file, r.URL.Query().Get("inline") != "1")
}

func (a *App) Thumbnail(w http.ResponseWriter, r *http.Request) {
	box, file, ok := a.loadFileForRequest(w, r)
	if !ok {
		return
	}
	if a.uploadService.IsProtected(box) && box.Obfuscate && !a.isBoxUnlocked(r, box) {
		a.servePlaceholderThumbnail(w, r)
		return
	}

	object, err := a.uploadService.OpenThumbnailObject(r.Context(), box, file)
	if err != nil {
		// The thumbnail isn't generated yet (background job pending). Serve the
		// placeholder but mark it non-cacheable, otherwise the browser would
		// keep showing the placeholder until a hard refresh once the real
		// thumbnail lands. The real thumbnail below is content-stable, so it
		// gets a long immutable cache.
		a.servePlaceholderThumbnail(w, r)
		return
	}
	defer object.Body.Close()
	w.Header().Set("Content-Type", "image/jpeg")
	w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
	http.ServeContent(w, r, file.ID+"-thumbnail.jpg", object.ModTime, readSeekCloser(object.Body))
}

// servePlaceholderThumbnail serves the fallback image with no-store so the
// browser re-requests on the next load and picks up the real thumbnail as soon
// as it has been generated.
func (a *App) servePlaceholderThumbnail(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Cache-Control", "no-store, must-revalidate")
	http.ServeFile(w, r, filepath.Join(a.cfg.StaticDir, "img", "file-placeholder.webp"))
}

func (a *App) UnlockBox(w http.ResponseWriter, r *http.Request) {
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
		return
	}
	if !a.uploadService.VerifyPassword(box, r.FormValue("password")) {
		a.logger.Warn("box unlock failed", "source", "user_activity", "severity", "warn", "code", 4011, "box_id", box.ID)
		http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
		return
	}
	http.SetCookie(w, &http.Cookie{
		Name:     unlockCookieName(box.ID),
		Value:    a.uploadService.UnlockToken(box),
		Path:     "/d/" + box.ID,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   r.TLS != nil,
		Expires:  box.ExpiresAt,
	})
	a.logger.Info("box unlocked", "source", "user_activity", "severity", "user_activity", "code", 2002, "box_id", box.ID)
	http.Redirect(w, r, fmt.Sprintf("/d/%s", box.ID), http.StatusSeeOther)
}

func (a *App) loadFileForRequest(w http.ResponseWriter, r *http.Request) (services.Box, services.File, bool) {
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return services.Box{}, services.File{}, false
	}
	if err := a.uploadService.CanDownload(box); err != nil {
		http.Error(w, err.Error(), statusForDownloadError(err))
		return services.Box{}, services.File{}, false
	}

	file, err := a.uploadService.FindFile(box, r.PathValue("fileID"))
	if err != nil {
		http.NotFound(w, r)
		return services.Box{}, services.File{}, false
	}
	return box, file, true
}

func (a *App) serveFileContent(w http.ResponseWriter, r *http.Request, box services.Box, file services.File, attachment bool) {
	object, err := a.uploadService.OpenFileObject(r.Context(), box, file)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer object.Body.Close()

	w.Header().Set("Content-Type", file.ContentType)
	if attachment {
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", file.Name))
	}
	if seeker, ok := object.Body.(io.ReadSeeker); ok {
		http.ServeContent(w, r, file.Name, object.ModTime, seeker)
	} else {
		if object.Size > 0 {
			w.Header().Set("Content-Length", fmt.Sprintf("%d", object.Size))
		}
		w.WriteHeader(http.StatusOK)
		_, _ = io.Copy(w, object.Body)
	}

	if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
		a.logger.Warn("failed to record file download", "source", "download", "severity", "warn", "code", 4002, "box_id", box.ID, "error", err.Error())
	}
}

func readSeekCloser(source io.ReadCloser) io.ReadSeeker {
	data, err := io.ReadAll(source)
	if err != nil {
		return bytes.NewReader(nil)
	}
	return bytes.NewReader(data)
}

func (a *App) DownloadZip(w http.ResponseWriter, r *http.Request) {
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}
	if err := a.uploadService.CanDownload(box); err != nil {
		http.Error(w, err.Error(), statusForDownloadError(err))
		return
	}
	if a.uploadService.IsProtected(box) && !a.isBoxUnlocked(r, box) {
		http.Error(w, "password required", http.StatusUnauthorized)
		return
	}

	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", "warpbox-"+box.ID+".zip"))
	w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))

	if err := a.uploadService.WriteZip(w, box); err != nil {
		a.logger.Error("zip download failed", "source", "download", "severity", "error", "code", 5002, "box_id", box.ID, "error", err.Error())
		return
	}
	if err := a.uploadService.RecordDownload(box.ID); err != nil && !errors.Is(err, os.ErrNotExist) {
		a.logger.Warn("failed to record zip download", "source", "download", "severity", "warn", "code", 4003, "box_id", box.ID, "error", err.Error())
	}
}

func (a *App) fileView(box services.Box, file services.File) fileView {
	return fileView{
		ID:           file.ID,
		Name:         file.Name,
		Size:         helpers.FormatBytes(file.Size),
		ContentType:  file.ContentType,
		PreviewKind:  file.PreviewKind,
		URL:          fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
		DownloadURL:  fmt.Sprintf("/d/%s/f/%s/download", box.ID, file.ID),
		ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID),
	}
}

func (a *App) isBoxUnlocked(r *http.Request, box services.Box) bool {
	if !a.uploadService.IsProtected(box) {
		return true
	}
	cookie, err := r.Cookie(unlockCookieName(box.ID))
	if err != nil {
		return false
	}
	return cookie.Value == a.uploadService.UnlockToken(box)
}

func unlockCookieName(boxID string) string {
	return "warpbox_unlock_" + strings.NewReplacer("-", "_", ".", "_").Replace(boxID)
}

func absoluteURL(r *http.Request, path string) string {
	if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
		return path
	}
	scheme := "http"
	if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
		scheme = "https"
	}
	return fmt.Sprintf("%s://%s%s", scheme, r.Host, path)
}
