package handlers

import (
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

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

const adminFilesDefaultPageSize = 50

var adminFilesPageSizes = []int{25, 50, 100, 200}

type adminFilesData struct {
	Stats          services.AdminStats
	Section        string
	PageTitle      string
	Boxes          []adminBoxView
	Query          string
	Sort           string
	Dir            string
	Page           int
	PerPage        int
	PerPageOptions []int
	TotalPages     int
	Total          int
	RangeFrom      int
	RangeTo        int
	Columns        []adminFilesColumn
	PageLinks      []adminFilesPageLink
	HasPrev        bool
	HasNext        bool
	PrevHref       string
	NextHref       string
}

// adminFilesQuery captures the listing state that every paginated link must
// preserve.
type adminFilesQuery struct {
	Query string
	Sort  string
	Dir   string
	Per   int
}

type adminFilesColumn struct {
	Label     string
	Href      string
	Sorted    bool
	Ascending bool
}

type adminFilesPageLink struct {
	Page   int
	Href   string
	Active bool
}

type adminBoxEditData struct {
	Section   string
	PageTitle string
	Box       adminBoxDetail
	Files     []adminBoxEditFile
	Notice    string
	Error     string
}

type adminBoxDetail struct {
	ID             string
	Owner          string
	CreatedAt      string
	ExpiresLabel   string
	ExpiresInput   string
	NeverExpires   bool
	MaxDownloads   int
	DownloadCount  int
	FileCount      int
	TotalSize      string
	BackendID      string
	Protected      bool
	Obfuscated     bool
}

type adminBoxEditFile struct {
	ID           string
	Name         string
	Size         string
	ContentType  string
	ThumbnailURL string
	DownloadURL  string
	HasPreview   bool
}

// adminFileRow is the sortable/filterable representation of a box.
type adminFileRow struct {
	ID             string
	Owner          string
	CreatedAt      time.Time
	ExpiresAt      time.Time
	FileCount      int
	DownloadCount  int
	MaxDownloads   int
	TotalSize      int64
	TotalSizeLabel string
	Protected      bool
	Expired        bool
}

func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}

	stats, err := a.uploadService.AdminStats()
	if err != nil {
		http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
		return
	}
	boxes, err := a.uploadService.AdminBoxes(0)
	if err != nil {
		http.Error(w, "unable to load boxes", http.StatusInternalServerError)
		return
	}

	ownerCache := map[string]string{}
	rows := make([]adminFileRow, 0, len(boxes))
	for _, box := range boxes {
		rows = append(rows, adminFileRow{
			ID:             box.ID,
			Owner:          a.boxOwnerLabel(box.OwnerID, ownerCache),
			CreatedAt:      box.CreatedAt,
			ExpiresAt:      box.ExpiresAt,
			FileCount:      box.FileCount,
			DownloadCount:  box.DownloadCount,
			MaxDownloads:   box.MaxDownloads,
			TotalSize:      box.TotalSize,
			TotalSizeLabel: box.TotalSizeLabel,
			Protected:      box.Protected,
			Expired:        box.Expired,
		})
	}

	query := strings.TrimSpace(r.URL.Query().Get("q"))
	if query != "" {
		needle := strings.ToLower(query)
		filtered := rows[:0:0]
		for _, row := range rows {
			if strings.Contains(strings.ToLower(row.ID), needle) || strings.Contains(strings.ToLower(row.Owner), needle) {
				filtered = append(filtered, row)
			}
		}
		rows = filtered
	}

	sortKey := adminFilesSortKey(r.URL.Query().Get("sort"))
	dir := r.URL.Query().Get("dir")
	if dir != "asc" {
		dir = "desc"
	}
	sortAdminFileRows(rows, sortKey, dir)

	perPage := normalizePageSize(r.URL.Query().Get("per"), adminFilesDefaultPageSize, adminFilesPageSizes)
	state := adminFilesQuery{Query: query, Sort: sortKey, Dir: dir, Per: perPage}

	total := len(rows)
	totalPages := (total + perPage - 1) / perPage
	if totalPages < 1 {
		totalPages = 1
	}
	page := 1
	if parsed, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && parsed > 1 {
		page = parsed
	}
	if page > totalPages {
		page = totalPages
	}
	start := (page - 1) * perPage
	if start > total {
		start = total
	}
	end := start + perPage
	if end > total {
		end = total
	}

	views := make([]adminBoxView, 0, end-start)
	for _, row := range rows[start:end] {
		views = append(views, adminBoxView{
			ID:             row.ID,
			Owner:          row.Owner,
			CreatedAt:      row.CreatedAt.Format("Jan 2, 2006 15:04"),
			ExpiresAt:      boxExpiryLabel(row.ExpiresAt, "Jan 2, 2006 15:04"),
			FileCount:      row.FileCount,
			TotalSizeLabel: row.TotalSizeLabel,
			DownloadCount:  row.DownloadCount,
			MaxDownloads:   row.MaxDownloads,
			Protected:      row.Protected,
			Expired:        row.Expired,
		})
	}

	rangeFrom := 0
	if total > 0 {
		rangeFrom = start + 1
	}

	a.renderPage(w, r, http.StatusOK, "admin_files.html", web.PageData{
		Title:       "Admin files",
		Description: "Manage Warpbox uploads.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminFilesData{
			Stats:          stats,
			Section:        "files",
			PageTitle:      "Files",
			Boxes:          views,
			Query:          query,
			Sort:           sortKey,
			Dir:            dir,
			Page:           page,
			PerPage:        perPage,
			PerPageOptions: adminFilesPageSizes,
			TotalPages:     totalPages,
			Total:          total,
			RangeFrom:      rangeFrom,
			RangeTo:        end,
			Columns:        adminFilesColumns(state, sortKey, dir),
			PageLinks:      adminFilesPageLinks(state, page, totalPages),
			HasPrev:        page > 1,
			HasNext:        page < totalPages,
			PrevHref:       adminFilesHref(state, page-1),
			NextHref:       adminFilesHref(state, page+1),
		},
	})
}

func (a *App) boxOwnerLabel(ownerID string, cache map[string]string) string {
	if ownerID == "" {
		return "Anonymous"
	}
	if label, ok := cache[ownerID]; ok {
		return label
	}
	label := "User"
	if user, err := a.authService.UserByID(ownerID); err == nil {
		label = user.Email
	}
	cache[ownerID] = label
	return label
}

func adminFilesSortKey(value string) string {
	switch value {
	case "id", "owner", "files", "size", "downloads", "expires", "created":
		return value
	default:
		return "created"
	}
}

func sortAdminFileRows(rows []adminFileRow, sortKey, dir string) {
	less := func(i, j int) bool {
		a, b := rows[i], rows[j]
		switch sortKey {
		case "id":
			return strings.ToLower(a.ID) < strings.ToLower(b.ID)
		case "owner":
			return strings.ToLower(a.Owner) < strings.ToLower(b.Owner)
		case "files":
			return a.FileCount < b.FileCount
		case "size":
			return a.TotalSize < b.TotalSize
		case "downloads":
			return a.DownloadCount < b.DownloadCount
		case "expires":
			return a.ExpiresAt.Before(b.ExpiresAt)
		default:
			return a.CreatedAt.Before(b.CreatedAt)
		}
	}
	sort.SliceStable(rows, func(i, j int) bool {
		if dir == "desc" {
			return less(j, i)
		}
		return less(i, j)
	})
}

func adminFilesColumns(state adminFilesQuery, sortKey, dir string) []adminFilesColumn {
	defs := []struct{ Key, Label string }{
		{"id", "Box"},
		{"owner", "Owner"},
		{"files", "Files"},
		{"size", "Size"},
		{"downloads", "Downloads"},
		{"created", "Created"},
		{"expires", "Expires"},
	}
	columns := make([]adminFilesColumn, 0, len(defs))
	for _, def := range defs {
		sorted := sortKey == def.Key
		nextDir := "asc"
		if sorted && dir == "asc" {
			nextDir = "desc"
		}
		colState := state
		colState.Sort = def.Key
		colState.Dir = nextDir
		columns = append(columns, adminFilesColumn{
			Label:     def.Label,
			Href:      adminFilesHref(colState, 1),
			Sorted:    sorted,
			Ascending: dir == "asc",
		})
	}
	return columns
}

func adminFilesPageLinks(state adminFilesQuery, page, totalPages int) []adminFilesPageLink {
	links := make([]adminFilesPageLink, 0, 5)
	const window = 2
	for p := page - window; p <= page+window; p++ {
		if p < 1 || p > totalPages {
			continue
		}
		links = append(links, adminFilesPageLink{
			Page:   p,
			Href:   adminFilesHref(state, p),
			Active: p == page,
		})
	}
	return links
}

func adminFilesHref(state adminFilesQuery, page int) string {
	values := url.Values{}
	if state.Query != "" {
		values.Set("q", state.Query)
	}
	if state.Sort != "" && state.Sort != "created" {
		values.Set("sort", state.Sort)
	}
	if state.Dir != "" && state.Dir != "desc" {
		values.Set("dir", state.Dir)
	}
	if state.Per > 0 && state.Per != adminFilesDefaultPageSize {
		values.Set("per", strconv.Itoa(state.Per))
	}
	if page > 1 {
		values.Set("page", strconv.Itoa(page))
	}
	if len(values) == 0 {
		return "/admin/files"
	}
	return "/admin/files?" + values.Encode()
}

// normalizePageSize parses a requested page size, falling back to def when the
// value is missing or not one of the allowed sizes.
func normalizePageSize(raw string, def int, allowed []int) int {
	parsed, err := strconv.Atoi(strings.TrimSpace(raw))
	if err != nil {
		return def
	}
	for _, size := range allowed {
		if size == parsed {
			return parsed
		}
	}
	return def
}

func (a *App) AdminEditBox(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) {
		return
	}
	box, err := a.uploadService.GetBox(r.PathValue("boxID"))
	if err != nil {
		http.NotFound(w, r)
		return
	}

	var totalSize int64
	files := make([]adminBoxEditFile, 0, len(box.Files))
	for _, file := range box.Files {
		totalSize += file.Size
		files = append(files, adminBoxEditFile{
			ID:           file.ID,
			Name:         file.Name,
			Size:         helpers.FormatBytes(file.Size),
			ContentType:  file.ContentType,
			ThumbnailURL: fmt.Sprintf("/d/%s/thumb/%s", box.ID, file.ID),
			DownloadURL:  fmt.Sprintf("/d/%s/f/%s", box.ID, file.ID),
			HasPreview:   file.PreviewKind == "image" || file.PreviewKind == "video",
		})
	}

	never := neverExpires(box.ExpiresAt)
	expiresInput := ""
	if !never {
		expiresInput = box.ExpiresAt.UTC().Format("2006-01-02T15:04")
	}

	cache := map[string]string{}
	a.renderPage(w, r, http.StatusOK, "admin_box_edit.html", web.PageData{
		Title:       "Edit box",
		Description: "Edit a Warpbox upload.",
		CurrentUser: a.currentPublicUser(r),
		Data: adminBoxEditData{
			Section:   "files",
			PageTitle: "Edit box",
			Notice:    r.URL.Query().Get("notice"),
			Error:     r.URL.Query().Get("error"),
			Files:     files,
			Box: adminBoxDetail{
				ID:            box.ID,
				Owner:         a.boxOwnerLabel(box.OwnerID, cache),
				CreatedAt:     box.CreatedAt.Format("Jan 2, 2006 15:04 MST"),
				ExpiresLabel:  boxExpiryLabel(box.ExpiresAt, "Jan 2, 2006 15:04 MST"),
				ExpiresInput:  expiresInput,
				NeverExpires:  never,
				MaxDownloads:  box.MaxDownloads,
				DownloadCount: box.DownloadCount,
				FileCount:     len(box.Files),
				TotalSize:     helpers.FormatBytes(totalSize),
				BackendID:     a.uploadService.BoxStorageBackendID(box),
				Protected:     a.uploadService.IsProtected(box),
				Obfuscated:    box.Obfuscate,
			},
		},
	})
}

func (a *App) AdminUpdateBox(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	boxID := r.PathValue("boxID")
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+read+form", http.StatusSeeOther)
		return
	}

	var expiresAt time.Time
	if r.FormValue("never_expires") == "on" {
		expiresAt = time.Now().UTC().AddDate(100, 0, 0)
	} else {
		parsed, err := time.Parse("2006-01-02T15:04", strings.TrimSpace(r.FormValue("expires_at")))
		if err != nil {
			http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Invalid+expiration+date", http.StatusSeeOther)
			return
		}
		expiresAt = parsed.UTC()
	}

	maxDownloads := parsePositiveInt(r.FormValue("max_downloads"))
	removePassword := r.FormValue("remove_password") == "on"

	if err := a.uploadService.AdminUpdateBox(boxID, expiresAt, maxDownloads, removePassword); err != nil {
		a.logger.Warn("admin box update failed", "source", "admin", "severity", "warn", "code", 4306, "box_id", boxID, "error", err.Error())
		http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+save+changes", http.StatusSeeOther)
		return
	}
	a.logger.Info("admin box updated", "source", "admin", "severity", "user_activity", "code", 2306, "ip", uploadClientIP(r), "box_id", boxID)
	http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?notice=Changes+saved", http.StatusSeeOther)
}

func (a *App) AdminDeleteBoxFile(w http.ResponseWriter, r *http.Request) {
	if !a.requireAdmin(w, r) || !a.validateCSRF(w, r) {
		return
	}
	boxID := r.PathValue("boxID")
	fileID := r.PathValue("fileID")
	boxDeleted, err := a.uploadService.RemoveFileFromBox(boxID, fileID)
	if err != nil {
		a.logger.Warn("admin file delete failed", "source", "admin", "severity", "warn", "code", 4305, "box_id", boxID, "file_id", fileID, "error", err.Error())
		http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?error=Could+not+remove+file", http.StatusSeeOther)
		return
	}
	a.logger.Info("admin removed box file", "source", "admin", "severity", "user_activity", "code", 2305, "ip", uploadClientIP(r), "box_id", boxID, "file_id", fileID)
	if boxDeleted {
		http.Redirect(w, r, "/admin/files?notice=Box+deleted+(last+file+removed)", http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/admin/boxes/"+boxID+"/edit?notice=File+removed", http.StatusSeeOther)
}
