package main

import (
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	"golang.org/x/crypto/bcrypt"

	"warpbox/lib/boxstore"
	"warpbox/lib/helpers"
	"warpbox/lib/models"
)

func newBoxCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "box",
		Short: "Manage boxes",
		Long:  "Manage WarpBox upload boxes: list, view, inspect, delete, modify.",
	}

	cmd.AddCommand(newBoxListCommand())
	cmd.AddCommand(newBoxViewCommand())
	cmd.AddCommand(newBoxInspectCommand())
	cmd.AddCommand(newBoxDeleteCommand())
	cmd.AddCommand(newBoxChangeCommand())
	cmd.AddCommand(newBoxGetCommand())

	return cmd
}

func newBoxListCommand() *cobra.Command {
	var format string
	var uploadRoot string
	var sortBy string
	var sortOrder string
	var filterExpired string
	var filterPassword string
	var filterOneTime string
	var filterSizeMin string
	var filterSizeMax string
	var filterCreatedAfter string
	var filterCreatedBefore string
	cmd := &cobra.Command{
		Use:     "ls",
		Aliases: []string{"list", "view"},
		Short:   "List all boxes",
		Long:    "List all boxes with optional sorting and filtering.",
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			summaries, err := boxstore.ListBoxSummaries()
			if err != nil {
				return fmt.Errorf("failed to list boxes: %w", err)
			}
			if len(summaries) == 0 {
				fmt.Println("No boxes found.")
				return nil
			}

			// Apply filters
			summaries = filterBoxes(summaries, filterExpired, filterPassword, filterOneTime, filterSizeMin, filterSizeMax, filterCreatedAfter, filterCreatedBefore)

			if len(summaries) == 0 {
				fmt.Println("No boxes match the given filters.")
				return nil
			}

			// Apply sorting
			sortBoxes(summaries, sortBy, sortOrder)

			switch format {
			case "json":
				return formatBoxSummariesJSON(summaries)
			case "table", "":
				return formatBoxSummariesTable(summaries)
			default:
				return fmt.Errorf("unknown format: %s (use 'table' or 'json')", format)
			}
		},
	}
	cmd.Flags().StringVarP(&format, "format", "o", "table", "Output format: table, json")
	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().StringVar(&sortBy, "sort", "created", "Sort field: created, expires, size, files")
	cmd.Flags().StringVar(&sortOrder, "sort-order", "desc", "Sort order: asc, desc")
	cmd.Flags().StringVar(&filterExpired, "filter-expired", "", "Filter by expiry: yes, no, all")
	cmd.Flags().StringVar(&filterPassword, "filter-password", "", "Filter by password: yes, no, all")
	cmd.Flags().StringVar(&filterOneTime, "filter-one-time", "", "Filter by one-time: yes, no, all")
	cmd.Flags().StringVar(&filterSizeMin, "filter-size-min", "", "Minimum total size in bytes (e.g. 1024, 1k, 1m, 1g)")
	cmd.Flags().StringVar(&filterSizeMax, "filter-size-max", "", "Maximum total size in bytes (e.g. 1024, 1k, 1m, 1g)")
	cmd.Flags().StringVar(&filterCreatedAfter, "filter-created-after", "", "Only boxes created after this time (RFC3339)")
	cmd.Flags().StringVar(&filterCreatedBefore, "filter-created-before", "", "Only boxes created before this time (RFC3339)")
	return cmd
}

func filterBoxes(summaries []models.BoxSummary, filterExpired, filterPassword, filterOneTime, filterSizeMin, filterSizeMax, filterCreatedAfter, filterCreatedBefore string) []models.BoxSummary {
	result := make([]models.BoxSummary, 0, len(summaries))

	minSize, _ := parseSizeFilter(filterSizeMin)
	maxSize, _ := parseSizeFilter(filterSizeMax)
	createdAfter, _ := time.Parse(time.RFC3339, filterCreatedAfter)
	createdBefore, _ := time.Parse(time.RFC3339, filterCreatedBefore)

	for _, s := range summaries {
		if filterExpired != "" && filterExpired != "all" {
			match := "no"
			if s.Expired {
				match = "yes"
			}
			if match != filterExpired {
				continue
			}
		}
		if filterPassword != "" && filterPassword != "all" {
			match := "no"
			if s.PasswordProtected {
				match = "yes"
			}
			if match != filterPassword {
				continue
			}
		}
		if filterOneTime != "" && filterOneTime != "all" {
			match := "no"
			if s.OneTimeDownload {
				match = "yes"
			}
			if match != filterOneTime {
				continue
			}
		}
		if minSize > 0 && s.TotalSize < minSize {
			continue
		}
		if maxSize > 0 && s.TotalSize > maxSize {
			continue
		}
		if !createdAfter.IsZero() && s.CreatedAt.Before(createdAfter) {
			continue
		}
		if !createdBefore.IsZero() && !s.CreatedAt.Before(createdBefore) {
			continue
		}
		result = append(result, s)
	}
	return result
}

func parseSizeFilter(s string) (int64, error) {
	if s == "" {
		return 0, nil
	}
	s = strings.TrimSpace(s)
	lower := strings.ToLower(s)

	multiplier := int64(1)
	switch {
	case strings.HasSuffix(lower, "g"):
		multiplier = 1024 * 1024 * 1024
		s = strings.TrimSuffix(lower, "g")
	case strings.HasSuffix(lower, "m"):
		multiplier = 1024 * 1024
		s = strings.TrimSuffix(lower, "m")
	case strings.HasSuffix(lower, "k"):
		multiplier = 1024
		s = strings.TrimSuffix(lower, "k")
	}

	val, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid size filter: %s", s)
	}
	return val * multiplier, nil
}

func sortBoxes(summaries []models.BoxSummary, sortBy, sortOrder string) {
	reverse := false
	if strings.EqualFold(sortOrder, "desc") {
		reverse = true
	}

	sort.SliceStable(summaries, func(i, j int) bool {
		var less bool
		switch strings.ToLower(sortBy) {
		case "size":
			less = summaries[i].TotalSize < summaries[j].TotalSize
		case "files":
			less = summaries[i].FileCount < summaries[j].FileCount
		case "expires":
			// Boxes with no expiry go last
			iZero := summaries[i].ExpiresAt.IsZero()
			jZero := summaries[j].ExpiresAt.IsZero()
			if iZero && jZero {
				return false
			}
			if iZero {
				return false
			}
			if jZero {
				return true
			}
			less = summaries[i].ExpiresAt.Before(summaries[j].ExpiresAt)
		case "created", "":
			less = summaries[i].CreatedAt.Before(summaries[j].CreatedAt)
		default:
			less = summaries[i].ID < summaries[j].ID
		}
		if reverse {
			return !less
		}
		return less
	})
}

func newBoxViewCommand() *cobra.Command {
	var uploadRoot string
	var asJSON bool
	cmd := &cobra.Command{
		Use:   "view",
		Short: "View box summary",
		Long:  "View a box summary showing files, size, expiry, etc.",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			boxID := args[0]
			summary, err := boxstore.BoxSummary(boxID)
			if err != nil {
				return fmt.Errorf("failed to view box %s: %w", boxID, err)
			}
			if asJSON {
				return formatBoxSummaryJSON(&summary)
			}
			printBoxSummary(&summary)
			return nil
		},
	}
	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
	return cmd
}

func newBoxInspectCommand() *cobra.Command {
	var uploadRoot string
	var full bool
	var asJSON bool
	cmd := &cobra.Command{
		Use:   "inspect",
		Short: "Inspect box manifest (raw JSON)",
		Long:  "Print the full box manifest as JSON. Use --full for hidden fields.",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			boxID := args[0]
			manifest, err := boxstore.ReadManifest(boxID)
			if err != nil {
				return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
			}
			if !full {
				sanitized := manifest
				sanitized.PasswordHash = "[REDACTED]"
				sanitized.PasswordSalt = "[REDACTED]"
				sanitized.AuthToken = "[REDACTED]"
				manifest = sanitized
			}
			enc := json.NewEncoder(os.Stdout)
			enc.SetIndent("", "  ")
			return enc.Encode(manifest)
		},
	}
	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().BoolVar(&full, "full", false, "Show sensitive fields (password hash, auth token)")
	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON (default for inspect)")
	_ = asJSON // inspect is always JSON; flag kept for consistency
	return cmd
}

func newBoxDeleteCommand() *cobra.Command {
	var uploadRoot string
	var force bool
	var asJSON bool
	cmd := &cobra.Command{
		Use:     "rm",
		Aliases: []string{"del", "delete"},
		Short:   "Delete a box",
		Args:    cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			boxID := args[0]
			if !force {
				fmt.Printf("This will permanently delete box %s and all its files.\n", boxID)
				fmt.Print("Confirm (y/N): ")
				var confirm string
				if _, err := fmt.Scanln(&confirm); err != nil {
					confirm = "n"
				}
				if strings.ToLower(strings.TrimSpace(confirm)) != "y" {
					if asJSON {
						fmt.Println(`{"deleted": false, "reason": "aborted"}`)
					} else {
						fmt.Println("Aborted.")
					}
					return nil
				}
			}
			if err := boxstore.DeleteBox(boxID); err != nil {
				if asJSON {
					fmt.Printf(`{"deleted": false, "error": "%s"}\n`, strings.ReplaceAll(err.Error(), `"`, `\"`))
				} else {
					return fmt.Errorf("failed to delete box %s: %w", boxID, err)
				}
				return nil
			}
			if asJSON {
				fmt.Printf(`{"deleted": true, "box_id": "%s"}\n`, boxID)
			} else {
				fmt.Printf("Box %s deleted.\n", boxID)
			}
			return nil
		},
	}
	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
	return cmd
}

func newBoxChangeCommand() *cobra.Command {
	var uploadRoot string
	var retention int64
	var retentionList bool
	var password string
	var zip bool
	var oneTime bool
	var renew bool
	var renewSeconds int64
	var asJSON bool

	cmd := &cobra.Command{
		Use:     "change",
		Aliases: []string{"update", "modify"},
		Short:   "Change box properties",
		Long:    "Change box properties: retention, password, zip, one-time download, renew expiry.",
		Args:    cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			boxID := args[0]

			if retentionList {
				printRetentionOptions()
				return nil
			}

			changes, err := gatherBoxChanges(cmd.Flags(), retention, password, zip, oneTime, renew, renewSeconds)
			if err != nil {
				return err
			}

			if len(changes) == 0 {
				fmt.Println("No changes specified. Use --retention, --password, --zip, --one-time, --renew, or --retention-list.")
				return nil
			}

			manifest, err := boxstore.ReadManifest(boxID)
			if err != nil {
				return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
			}

			for _, apply := range changes {
				if err := apply(&manifest); err != nil {
					return err
				}
			}

			if err := boxstore.WriteManifest(boxID, manifest); err != nil {
				return fmt.Errorf("failed to save manifest for box %s: %w", boxID, err)
			}

			if asJSON {
				return formatChangeResultJSON(boxID, manifest)
			}
			fmt.Printf("Box %s updated.\n", boxID)
			return nil
		},
	}

	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().Int64Var(&retention, "retention", 0, "Set retention seconds (use --retention-list for valid values)")
	cmd.Flags().BoolVar(&retentionList, "retention-list", false, "List available retention options")
	cmd.Flags().StringVar(&password, "password", "", "Set a new password (empty string to remove)")
	cmd.Flags().BoolVar(&zip, "zip", true, "Allow ZIP downloads (default true, --zip=false to disable)")
	cmd.Flags().BoolVar(&oneTime, "one-time", false, "Enable one-time download mode")
	cmd.Flags().BoolVar(&renew, "renew", false, "Renew box expiry (use --renew-seconds for duration)")
	cmd.Flags().Int64Var(&renewSeconds, "renew-seconds", 0, "Seconds to extend expiry by (used with --renew)")
	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")

	return cmd
}

type changeFunc func(*models.BoxManifest) error

func gatherBoxChanges(flags *pflag.FlagSet, retention int64, password string, zip bool, oneTime bool, renew bool, renewSeconds int64) ([]changeFunc, error) {
	var changes []changeFunc

	if flags.Changed("retention") {
		if retention < 0 {
			return nil, fmt.Errorf("retention cannot be negative")
		}
		changes = append(changes, func(m *models.BoxManifest) error {
			if m.OneTimeDownload {
				m.OneTimeDownload = false
			}
			m.RetentionSecs = retention
			for _, opt := range boxstore.RetentionOptions() {
				if opt.Seconds == retention {
					m.RetentionKey = opt.Key
					m.RetentionLabel = opt.Label
					return nil
				}
			}
			m.RetentionKey = "custom"
			return nil
		})
	}

	if flags.Changed("password") {
		changes = append(changes, func(m *models.BoxManifest) error {
			if password == "" {
				m.PasswordHash = ""
				m.PasswordHashAlg = ""
				m.AuthToken = ""
				return nil
			}
			token, err := helpers.RandomHexID(16)
			if err != nil {
				return fmt.Errorf("could not generate auth token")
			}
			hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
			if err != nil {
				return fmt.Errorf("could not hash password: %w", err)
			}
			m.PasswordHash = string(hash)
			m.PasswordHashAlg = "bcrypt"
			m.AuthToken = token
			return nil
		})
	}

	if flags.Changed("zip") {
		changes = append(changes, func(m *models.BoxManifest) error {
			if m.OneTimeDownload {
				return nil
			}
			m.DisableZip = !zip
			return nil
		})
	}

	if flags.Changed("one-time") {
		changes = append(changes, func(m *models.BoxManifest) error {
			if oneTime {
				m.OneTimeDownload = true
				m.DisableZip = false
				if boxstore.OneTimeDownloadExpiry() > 0 {
					m.RetentionSecs = boxstore.OneTimeDownloadExpiry()
				}
			} else {
				m.OneTimeDownload = false
			}
			return nil
		})
	}

	if flags.Changed("renew") {
		changes = append(changes, func(m *models.BoxManifest) error {
			secs := renewSeconds
			if secs <= 0 {
				secs = m.RetentionSecs
			}
			return renewBoxExpiry(m, secs)
		})
	}

	return changes, nil
}

func renewBoxExpiry(m *models.BoxManifest, seconds int64) error {
	if seconds <= 0 || m.OneTimeDownload {
		return nil
	}
	if m.ExpiresAt.IsZero() {
		m.ExpiresAt = time.Now().UTC().Add(time.Duration(seconds) * time.Second)
		return nil
	}
	m.ExpiresAt = m.ExpiresAt.Add(time.Duration(seconds) * time.Second)
	return nil
}

func newBoxGetCommand() *cobra.Command {
	var uploadRoot string
	var asJSON bool
	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get box URL and info",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			if uploadRoot != "" {
				boxstore.SetUploadRoot(uploadRoot)
			}
			boxID := args[0]
			manifest, err := boxstore.ReadManifest(boxID)
			if err != nil {
				return fmt.Errorf("failed to read manifest for box %s: %w", boxID, err)
			}

			if asJSON {
				return formatBoxGetJSON(boxID, manifest)
			}

			fmt.Printf("Box ID:\t%s\n", boxID)
			fmt.Printf("URL:\t/box/%s\n", boxID)
			if !manifest.CreatedAt.IsZero() {
				fmt.Printf("Created:\t%s\n", manifest.CreatedAt.Format(time.RFC3339))
			}
			if !manifest.ExpiresAt.IsZero() {
				fmt.Printf("Expires:\t%s\n", manifest.ExpiresAt.Format(time.RFC3339))
			}
			if boxstore.IsPasswordProtected(manifest) {
				fmt.Println("Password:\tprotected")
			}
			if manifest.OneTimeDownload {
				fmt.Println("Mode:\tone-time download")
			}
			return nil
		},
	}
	cmd.Flags().StringVar(&uploadRoot, "upload-root", "", "Override upload root directory")
	cmd.Flags().BoolVar(&asJSON, "json", false, "Output as JSON")
	return cmd
}
