package server

import (
	"crypto/subtle"
	"errors"
	"fmt"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"warpbox/lib/boxstore"
	"warpbox/lib/config"
	"warpbox/lib/helpers"
	"warpbox/lib/metastore"
)

const adminSessionCookie = "warpbox_admin_session"

type adminUserRow struct {
	ID        string
	Username  string
	Email     string
	Tags      string
	CreatedAt string
	Disabled  bool
	IsCurrent bool
}

type adminTagRow struct {
	ID                     string
	Name                   string
	Description            string
	Protected              bool
	AdminAccess            bool
	UploadAllowed          bool
	ZipDownloadAllowed     bool
	OneTimeDownloadAllowed bool
	RenewableAllowed       bool
	MaxFileSizeBytes       string
	MaxBoxSizeBytes        string
	AllowedExpirySeconds   string
}

type adminBoxRow struct {
	ID                string
	FileCount         int
	TotalSizeLabel    string
	CreatedAt         string
	ExpiresAt         string
	Expired           bool
	OneTimeDownload   bool
	PasswordProtected bool
}

func (app *App) registerAdminRoutes(router *gin.Engine) {
	admin := router.Group("/admin")
	admin.Use(noStoreAdminHeaders)
	admin.GET("/login", app.handleAdminLogin)
	admin.POST("/login", app.handleAdminLoginPost)

	protected := admin.Group("")
	protected.Use(app.requireAdminSession)
	protected.POST("/logout", app.handleAdminLogout)
	protected.GET("", app.handleAdminDashboard)
	protected.GET("/", app.handleAdminDashboard)
	protected.GET("/boxes", app.handleAdminBoxes)
	protected.GET("/users", app.handleAdminUsers)
	protected.POST("/users", app.handleAdminUsersPost)
	protected.GET("/tags", app.handleAdminTags)
	protected.POST("/tags", app.handleAdminTagsPost)
	protected.GET("/settings", app.handleAdminSettings)
	protected.POST("/settings", app.handleAdminSettingsPost)
}

func (app *App) handleAdminLogin(ctx *gin.Context) {
	if app.isAdminSessionValid(ctx) {
		ctx.Redirect(http.StatusSeeOther, "/admin")
		return
	}
	app.renderAdminLogin(ctx, "")
}

func (app *App) handleAdminLoginPost(ctx *gin.Context) {
	if !app.adminLoginEnabled {
		app.renderAdminLogin(ctx, "Administrator login is disabled.")
		return
	}

	username := strings.TrimSpace(ctx.PostForm("username"))
	password := ctx.PostForm("password")
	user, ok, err := app.store.GetUserByUsername(username)
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not load user")
		return
	}
	if !ok || user.Disabled || !metastore.VerifyPassword(user.PasswordHash, password) {
		app.renderAdminLogin(ctx, "The username or password was not accepted.")
		return
	}

	perms, err := app.permissionsForUser(user)
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not load permissions")
		return
	}
	if !perms.AdminAccess {
		app.renderAdminLogin(ctx, "This user does not have administrator access.")
		return
	}

	session, err := app.store.CreateSession(user.ID, time.Duration(app.config.SessionTTLSeconds)*time.Second)
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not create session")
		return
	}
	ctx.SetSameSite(http.SameSiteLaxMode)
	ctx.SetCookie(adminSessionCookie, session.Token, int(app.config.SessionTTLSeconds), "/admin", "", app.config.AdminCookieSecure, true)
	ctx.Redirect(http.StatusSeeOther, "/admin")
}

func (app *App) handleAdminLogout(ctx *gin.Context) {
	if token, err := ctx.Cookie(adminSessionCookie); err == nil {
		_ = app.store.DeleteSession(token)
	}
	ctx.SetSameSite(http.SameSiteLaxMode)
	ctx.SetCookie(adminSessionCookie, "", -1, "/admin", "", app.config.AdminCookieSecure, true)
	ctx.Redirect(http.StatusSeeOther, "/admin/login")
}

func (app *App) handleAdminDashboard(ctx *gin.Context) {
	ctx.HTML(http.StatusOK, "admin.html", gin.H{
		"CurrentUser": app.currentAdminUsername(ctx),
		"CSRFToken":   app.currentCSRFToken(ctx),
	})
}

func (app *App) handleAdminBoxes(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminBoxesView }) {
		return
	}

	summaries, err := boxstore.ListBoxSummaries()
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not list boxes")
		return
	}

	rows := make([]adminBoxRow, 0, len(summaries))
	totalSize := int64(0)
	expiredCount := 0
	for _, summary := range summaries {
		totalSize += summary.TotalSize
		if summary.Expired {
			expiredCount++
		}
		rows = append(rows, adminBoxRow{
			ID:                summary.ID,
			FileCount:         summary.FileCount,
			TotalSizeLabel:    summary.TotalSizeLabel,
			CreatedAt:         formatAdminTime(summary.CreatedAt),
			ExpiresAt:         formatAdminTime(summary.ExpiresAt),
			Expired:           summary.Expired,
			OneTimeDownload:   summary.OneTimeDownload,
			PasswordProtected: summary.PasswordProtected,
		})
	}

	ctx.HTML(http.StatusOK, "admin_boxes.html", gin.H{
		"CurrentUser":  app.currentAdminUsername(ctx),
		"Boxes":        rows,
		"TotalBoxes":   len(rows),
		"TotalStorage": helpers.FormatBytes(totalSize),
		"ExpiredBoxes": expiredCount,
	})
}

func (app *App) handleAdminUsers(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminUsersManage }) {
		return
	}
	app.renderAdminUsers(ctx, "")
}

func (app *App) handleAdminUsersPost(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminUsersManage }) {
		return
	}

	if ctx.PostForm("action") == "toggle_disabled" {
		userID := strings.TrimSpace(ctx.PostForm("user_id"))
		user, ok, err := app.store.GetUser(userID)
		if err != nil || !ok {
			app.renderAdminUsers(ctx, "User not found.")
			return
		}
		if current, ok := ctx.Get("adminUser"); ok {
			if currentUser, ok := current.(metastore.User); ok && currentUser.ID == user.ID {
				app.renderAdminUsers(ctx, "You cannot disable the user for the active session.")
				return
			}
		}
		user.Disabled = !user.Disabled
		if err := app.store.UpdateUser(user); err != nil {
			app.renderAdminUsers(ctx, err.Error())
			return
		}
		ctx.Redirect(http.StatusSeeOther, "/admin/users")
		return
	}

	username := ctx.PostForm("username")
	email := ctx.PostForm("email")
	password := ctx.PostForm("password")
	tagIDs := ctx.PostFormArray("tag_ids")
	if _, err := app.store.CreateUserWithPassword(username, email, password, tagIDs); err != nil {
		app.renderAdminUsers(ctx, err.Error())
		return
	}
	ctx.Redirect(http.StatusSeeOther, "/admin/users")
}

func (app *App) renderAdminUsers(ctx *gin.Context, errorMessage string) {
	users, err := app.store.ListUsers()
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not list users")
		return
	}
	tags, err := app.store.ListTags()
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not list tags")
		return
	}
	tagNames := make(map[string]string, len(tags))
	for _, tag := range tags {
		tagNames[tag.ID] = tag.Name
	}
	sort.Slice(users, func(i int, j int) bool {
		return strings.ToLower(users[i].Username) < strings.ToLower(users[j].Username)
	})

	currentID := ""
	if current, ok := ctx.Get("adminUser"); ok {
		if currentUser, ok := current.(metastore.User); ok {
			currentID = currentUser.ID
		}
	}

	rows := make([]adminUserRow, 0, len(users))
	for _, user := range users {
		names := make([]string, 0, len(user.TagIDs))
		for _, tagID := range user.TagIDs {
			if name := tagNames[tagID]; name != "" {
				names = append(names, name)
			}
		}
		rows = append(rows, adminUserRow{
			ID:        user.ID,
			Username:  user.Username,
			Email:     user.Email,
			Tags:      strings.Join(names, ", "),
			CreatedAt: formatAdminTime(user.CreatedAt),
			Disabled:  user.Disabled,
			IsCurrent: user.ID == currentID,
		})
	}

	ctx.HTML(http.StatusOK, "admin_users.html", gin.H{
		"CurrentUser": app.currentAdminUsername(ctx),
		"CSRFToken":   app.currentCSRFToken(ctx),
		"Users":       rows,
		"Tags":        tags,
		"Error":       errorMessage,
	})
}

func (app *App) handleAdminTags(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminUsersManage }) {
		return
	}
	app.renderAdminTags(ctx, "")
}

func (app *App) handleAdminTagsPost(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminUsersManage }) {
		return
	}

	perms, err := parseTagPermissions(ctx)
	if err != nil {
		app.renderAdminTags(ctx, err.Error())
		return
	}
	tag := metastore.Tag{
		Name:        ctx.PostForm("name"),
		Description: ctx.PostForm("description"),
		Permissions: perms,
	}
	if err := app.store.CreateTag(&tag); err != nil {
		app.renderAdminTags(ctx, err.Error())
		return
	}
	ctx.Redirect(http.StatusSeeOther, "/admin/tags")
}

func (app *App) renderAdminTags(ctx *gin.Context, errorMessage string) {
	tags, err := app.store.ListTags()
	if err != nil {
		ctx.String(http.StatusInternalServerError, "Could not list tags")
		return
	}
	sort.Slice(tags, func(i int, j int) bool {
		return strings.ToLower(tags[i].Name) < strings.ToLower(tags[j].Name)
	})
	rows := make([]adminTagRow, 0, len(tags))
	for _, tag := range tags {
		rows = append(rows, adminTagRow{
			ID:                     tag.ID,
			Name:                   tag.Name,
			Description:            tag.Description,
			Protected:              tag.Protected,
			AdminAccess:            tag.Permissions.AdminAccess,
			UploadAllowed:          tag.Permissions.UploadAllowed,
			ZipDownloadAllowed:     tag.Permissions.ZipDownloadAllowed,
			OneTimeDownloadAllowed: tag.Permissions.OneTimeDownloadAllowed,
			RenewableAllowed:       tag.Permissions.RenewableAllowed,
			MaxFileSizeBytes:       optionalInt64Label(tag.Permissions.MaxFileSizeBytes),
			MaxBoxSizeBytes:        optionalInt64Label(tag.Permissions.MaxBoxSizeBytes),
			AllowedExpirySeconds:   joinInt64s(tag.Permissions.AllowedExpirySeconds),
		})
	}
	ctx.HTML(http.StatusOK, "admin_tags.html", gin.H{
		"CurrentUser": app.currentAdminUsername(ctx),
		"CSRFToken":   app.currentCSRFToken(ctx),
		"Tags":        rows,
		"Error":       errorMessage,
	})
}

func (app *App) handleAdminSettings(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminSettingsManage }) {
		return
	}
	app.renderAdminSettings(ctx, "")
}

func (app *App) handleAdminSettingsPost(ctx *gin.Context) {
	if !app.requireAdminFlag(ctx, func(perms metastore.EffectivePermissions) bool { return perms.AdminSettingsManage }) {
		return
	}
	if !app.config.AllowAdminSettingsOverride {
		app.renderAdminSettings(ctx, "Admin settings overrides are disabled by environment configuration.")
		return
	}

	for _, def := range config.EditableDefinitions() {
		value := ctx.PostForm(def.Key)
		if def.Type == config.SettingTypeBool {
			value = "false"
			if ctx.PostForm(def.Key) == "true" {
				value = "true"
			}
		}
		if err := app.config.ApplyOverride(def.Key, value); err != nil {
			app.renderAdminSettings(ctx, err.Error())
			return
		}
		if err := app.store.SetSetting(def.Key, value); err != nil {
			app.renderAdminSettings(ctx, err.Error())
			return
		}
	}
	ctx.Redirect(http.StatusSeeOther, "/admin/settings")
}

func (app *App) renderAdminSettings(ctx *gin.Context, errorMessage string) {
	ctx.HTML(http.StatusOK, "admin_settings.html", gin.H{
		"CurrentUser":      app.currentAdminUsername(ctx),
		"CSRFToken":        app.currentCSRFToken(ctx),
		"Rows":             app.config.SettingRows(),
		"OverridesAllowed": app.config.AllowAdminSettingsOverride,
		"Error":            errorMessage,
	})
}

func (app *App) requireAdminSession(ctx *gin.Context) {
	token, err := ctx.Cookie(adminSessionCookie)
	if err != nil {
		ctx.Redirect(http.StatusSeeOther, "/admin/login")
		ctx.Abort()
		return
	}
	session, ok, err := app.store.GetSession(token)
	if err != nil || !ok {
		ctx.Redirect(http.StatusSeeOther, "/admin/login")
		ctx.Abort()
		return
	}
	if !validAdminCSRF(ctx, session) {
		ctx.String(http.StatusForbidden, "Permission denied")
		ctx.Abort()
		return
	}
	user, ok, err := app.store.GetUser(session.UserID)
	if err != nil || !ok || user.Disabled {
		ctx.Redirect(http.StatusSeeOther, "/admin/login")
		ctx.Abort()
		return
	}
	perms, err := app.permissionsForUser(user)
	if err != nil || !perms.AdminAccess {
		ctx.Redirect(http.StatusSeeOther, "/admin/login")
		ctx.Abort()
		return
	}
	ctx.Set("adminUser", user)
	ctx.Set("adminPerms", perms)
	ctx.Set("adminCSRFToken", session.CSRFToken)
	ctx.Next()
}

func (app *App) isAdminSessionValid(ctx *gin.Context) bool {
	token, err := ctx.Cookie(adminSessionCookie)
	if err != nil {
		return false
	}
	session, ok, err := app.store.GetSession(token)
	if err != nil || !ok {
		return false
	}
	user, ok, err := app.store.GetUser(session.UserID)
	if err != nil || !ok || user.Disabled {
		return false
	}
	perms, err := app.permissionsForUser(user)
	return err == nil && perms.AdminAccess
}

func (app *App) permissionsForUser(user metastore.User) (metastore.EffectivePermissions, error) {
	tags, err := app.store.TagsByID(user.TagIDs)
	if err != nil {
		return metastore.EffectivePermissions{}, err
	}
	return metastore.ResolveUserPermissions(app.config, user, tags), nil
}

func (app *App) requireAdminFlag(ctx *gin.Context, allowed func(metastore.EffectivePermissions) bool) bool {
	value, ok := ctx.Get("adminPerms")
	if !ok {
		ctx.String(http.StatusForbidden, "Permission denied")
		return false
	}
	perms, ok := value.(metastore.EffectivePermissions)
	if !ok || !allowed(perms) {
		ctx.String(http.StatusForbidden, "Permission denied")
		return false
	}
	return true
}

func (app *App) currentAdminUsername(ctx *gin.Context) string {
	if current, ok := ctx.Get("adminUser"); ok {
		if user, ok := current.(metastore.User); ok {
			return user.Username
		}
	}
	return ""
}

func (app *App) currentCSRFToken(ctx *gin.Context) string {
	if value, ok := ctx.Get("adminCSRFToken"); ok {
		if token, ok := value.(string); ok {
			return token
		}
	}
	return ""
}

func (app *App) renderAdminLogin(ctx *gin.Context, errorMessage string) {
	ctx.HTML(http.StatusOK, "admin_login.html", gin.H{
		"AdminLoginEnabled": app.adminLoginEnabled,
		"Error":             errorMessage,
	})
}

func noStoreAdminHeaders(ctx *gin.Context) {
	ctx.Header("Cache-Control", "no-store")
	ctx.Header("Pragma", "no-cache")
	ctx.Header("X-Content-Type-Options", "nosniff")
	ctx.Next()
}

func validAdminCSRF(ctx *gin.Context, session metastore.Session) bool {
	switch ctx.Request.Method {
	case http.MethodGet, http.MethodHead, http.MethodOptions:
		return true
	}

	token := ctx.PostForm("csrf_token")
	return token != "" && subtleConstantTimeEqual(token, session.CSRFToken)
}

func subtleConstantTimeEqual(a string, b string) bool {
	if len(a) != len(b) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

func parseTagPermissions(ctx *gin.Context) (metastore.TagPermissions, error) {
	maxFileSize, err := parseOptionalInt64(ctx.PostForm("max_file_size_bytes"))
	if err != nil {
		return metastore.TagPermissions{}, fmt.Errorf("max file size bytes %w", err)
	}
	maxBoxSize, err := parseOptionalInt64(ctx.PostForm("max_box_size_bytes"))
	if err != nil {
		return metastore.TagPermissions{}, fmt.Errorf("max box size bytes %w", err)
	}
	expirySeconds, err := parseCSVInt64(ctx.PostForm("allowed_expiry_seconds"))
	if err != nil {
		return metastore.TagPermissions{}, err
	}
	return metastore.TagPermissions{
		UploadAllowed:          checkbox(ctx, "upload_allowed"),
		AllowedExpirySeconds:   expirySeconds,
		MaxFileSizeBytes:       maxFileSize,
		MaxBoxSizeBytes:        maxBoxSize,
		OneTimeDownloadAllowed: checkbox(ctx, "one_time_download_allowed"),
		ZipDownloadAllowed:     checkbox(ctx, "zip_download_allowed"),
		RenewableAllowed:       checkbox(ctx, "renewable_allowed"),
		AdminAccess:            checkbox(ctx, "admin_access"),
		AdminUsersManage:       checkbox(ctx, "admin_users_manage"),
		AdminSettingsManage:    checkbox(ctx, "admin_settings_manage"),
		AdminBoxesView:         checkbox(ctx, "admin_boxes_view"),
	}, nil
}

func checkbox(ctx *gin.Context, name string) bool {
	return ctx.PostForm(name) == "true"
}

func parseOptionalInt64(raw string) (*int64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	value, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return nil, errors.New("must be an integer")
	}
	if value < 0 {
		return nil, errors.New("must be at least 0")
	}
	return &value, nil
}

func parseCSVInt64(raw string) ([]int64, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, nil
	}
	parts := strings.Split(raw, ",")
	values := make([]int64, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}
		value, err := strconv.ParseInt(part, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("allowed expiry durations must be comma-separated seconds")
		}
		if value < 0 {
			return nil, fmt.Errorf("allowed expiry durations must be at least 0")
		}
		values = append(values, value)
	}
	return values, nil
}

func optionalInt64Label(value *int64) string {
	if value == nil {
		return "-"
	}
	return strconv.FormatInt(*value, 10)
}

func joinInt64s(values []int64) string {
	if len(values) == 0 {
		return "-"
	}
	parts := make([]string, 0, len(values))
	for _, value := range values {
		parts = append(parts, strconv.FormatInt(value, 10))
	}
	return strings.Join(parts, ", ")
}

func formatAdminTime(value time.Time) string {
	if value.IsZero() {
		return "-"
	}
	return value.Local().Format("2006-01-02 15:04:05")
}
