package main

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"text/tabwriter"
	"time"

	"git.warky.dev/wdevs/whatshooked/pkg/config"
	"git.warky.dev/wdevs/whatshooked/pkg/models"
	"git.warky.dev/wdevs/whatshooked/pkg/storage"
	resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
	"github.com/google/uuid"
	"github.com/spf13/cobra"
	"golang.org/x/crypto/bcrypt"
	"golang.org/x/term"
)

var serverConfigPath string

// flags for add
var (
	addUsername string
	addEmail    string
	addFullName string
	addPassword string
	addRole     string
)

// flags for update
var (
	updateUsername string
	updateEmail    string
	updateFullName string
	updateRole     string
)

var usersCmd = &cobra.Command{
	Use:   "users",
	Short: "Manage users (direct DB access)",
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		initUserDB()
	},
	Run: func(cmd *cobra.Command, args []string) {
		listUsers()
	},
}

var usersListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all users",
	Run: func(cmd *cobra.Command, args []string) {
		listUsers()
	},
}

var usersAddCmd = &cobra.Command{
	Use:   "add",
	Short: "Add a new user",
	Run: func(cmd *cobra.Command, args []string) {
		addUser()
	},
}

var usersSetPasswordCmd = &cobra.Command{
	Use:   "set-password <username> [password]",
	Short: "Change a user's password",
	Args:  cobra.RangeArgs(1, 2),
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) == 2 {
			setPasswordDirect(args[0], args[1])
		} else {
			setPassword(args[0])
		}
	},
}

var usersDisableCmd = &cobra.Command{
	Use:   "disable <username>",
	Short: "Disable a user account",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		setUserActive(args[0], false)
	},
}

var usersEnableCmd = &cobra.Command{
	Use:   "enable <username>",
	Short: "Enable a user account",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		setUserActive(args[0], true)
	},
}

var usersUpdateCmd = &cobra.Command{
	Use:   "update <username>",
	Short: "Update a user's details",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		updateUser(args[0])
	},
}

var usersDeleteCmd = &cobra.Command{
	Use:   "delete <username>",
	Short: "Delete a user",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		deleteUser(args[0])
	},
}

var usersRemoveCmd = &cobra.Command{
	Use:   "remove <username>",
	Short: "Remove a user (alias for delete)",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		deleteUser(args[0])
	},
}

func init() {
	usersCmd.PersistentFlags().StringVar(&serverConfigPath, "server-config", "", "server config file (default: config.json, ../config.json, or ~/.whatshooked/config.json)")

	usersAddCmd.Flags().StringVarP(&addUsername, "username", "u", "", "Username")
	usersAddCmd.Flags().StringVarP(&addEmail, "email", "e", "", "Email address")
	usersAddCmd.Flags().StringVarP(&addFullName, "full-name", "n", "", "Full name")
	usersAddCmd.Flags().StringVarP(&addPassword, "password", "p", "", "Password")
	usersAddCmd.Flags().StringVarP(&addRole, "role", "r", "", "Role (admin/user)")

	usersUpdateCmd.Flags().StringVarP(&updateUsername, "username", "u", "", "New username")
	usersUpdateCmd.Flags().StringVarP(&updateEmail, "email", "e", "", "New email address")
	usersUpdateCmd.Flags().StringVarP(&updateFullName, "full-name", "n", "", "New full name")
	usersUpdateCmd.Flags().StringVarP(&updateRole, "role", "r", "", "New role (admin/user)")

	usersCmd.AddCommand(usersListCmd)
	usersCmd.AddCommand(usersAddCmd)
	usersCmd.AddCommand(usersUpdateCmd)
	usersCmd.AddCommand(usersSetPasswordCmd)
	usersCmd.AddCommand(usersDisableCmd)
	usersCmd.AddCommand(usersEnableCmd)
	usersCmd.AddCommand(usersDeleteCmd)
	usersCmd.AddCommand(usersRemoveCmd)
}

func resolveServerConfigPath() string {
	if serverConfigPath != "" {
		return serverConfigPath
	}
	if _, err := os.Stat("config.json"); err == nil {
		return "config.json"
	}
	if _, err := os.Stat("../config.json"); err == nil {
		return "../config.json"
	}
	home, err := os.UserHomeDir()
	if err == nil {
		p := filepath.Join(home, ".whatshooked", "config.json")
		if _, err := os.Stat(p); err == nil {
			return p
		}
	}
	return ""
}

// tryInitDB attempts to initialize the DB, returning false if it cannot.
func tryInitDB() bool {
	if storage.DB != nil {
		return true
	}
	cfgPath := resolveServerConfigPath()
	if cfgPath == "" {
		return false
	}
	cfg, err := config.Load(cfgPath)
	if err != nil {
		return false
	}
	return storage.Initialize(&cfg.Database) == nil
}

// dbOwnerUserID returns the ID of the first admin user (or first user) for use
// when creating records via DB that require a user_id.
func dbOwnerUserID(username string) string {
	var user models.ModelPublicUsers
	q := storage.DB.NewSelect().Model(&user)
	if username != "" {
		q = q.Where("username = ?", username)
	} else {
		q = q.Where("role = ?", "admin")
	}
	if err := q.Limit(1).Scan(context.Background()); err != nil {
		// fall back to any user
		if err2 := storage.DB.NewSelect().Model(&user).Limit(1).Scan(context.Background()); err2 != nil {
			return ""
		}
	}
	return user.ID.String()
}

func initUserDB() {
	cfgPath := resolveServerConfigPath()
	if cfgPath == "" {
		fmt.Fprintln(os.Stderr, "Error: server config not found. Use --server-config to specify path.")
		os.Exit(1)
	}
	cfg, err := config.Load(cfgPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error loading server config %s: %v\n", cfgPath, err)
		os.Exit(1)
	}
	if err := storage.Initialize(&cfg.Database); err != nil {
		fmt.Fprintf(os.Stderr, "Error connecting to database: %v\n", err)
		os.Exit(1)
	}
}

func listUsers() {
	var users []models.ModelPublicUsers
	err := storage.DB.NewSelect().Model(&users).OrderExpr("created_at ASC").Scan(context.Background())
	checkError(err)

	if len(users) == 0 {
		fmt.Println("No users found")
		return
	}

	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "USERNAME\tEMAIL\tFULL NAME\tROLE\tACTIVE\tCREATED")
	for _, u := range users {
		active := "yes"
		if !u.Active {
			active = "no"
		}
		created := fmt.Sprint(u.CreatedAt)
		if len(created) > 10 {
			created = created[:10]
		}
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
			u.Username.String(),
			u.Email.String(),
			u.FullName.String(),
			u.Role.String(),
			active,
			created,
		)
	}
	w.Flush()
}

func addUser() {
	username := addUsername
	if username == "" {
		username = promptRequired("Username")
	}

	email := addEmail
	if email == "" {
		email = promptRequired("Email")
	}

	fullName := addFullName
	if fullName == "" {
		fullName = promptLine("Full Name (optional)", "")
	}

	role := addRole
	if role == "" {
		role = promptLine("Role (admin/user)", "user")
	}

	password := addPassword
	if password == "" {
		password = readPassword("Password: ")
	}

	hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	checkError(err)

	now := time.Now()
	user := &models.ModelPublicUsers{
		ID:        resolvespec_common.NewSqlString(uuid.New().String()),
		Username:  resolvespec_common.NewSqlString(username),
		Email:     resolvespec_common.NewSqlString(email),
		FullName:  resolvespec_common.NewSqlString(fullName),
		Role:      resolvespec_common.NewSqlString(role),
		Password:  resolvespec_common.NewSqlString(string(hashedPw)),
		Active:    true,
		CreatedAt: resolvespec_common.NewSqlTimeStamp(now),
		UpdatedAt: resolvespec_common.NewSqlTimeStamp(now),
	}

	_, err = storage.DB.NewInsert().Model(user).Exec(context.Background())
	checkError(err)

	fmt.Printf("User '%s' created\n", username)
}

func setPasswordDirect(username, password string) {
	user := findUserByUsername(username)
	hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	checkError(err)
	_, err = storage.DB.NewUpdate().Model(user).
		Set("password = ?", string(hashedPw)).
		Set("updated_at = ?", time.Now()).
		Where("id = ?", user.ID.String()).
		Exec(context.Background())
	checkError(err)
	fmt.Printf("Password updated for '%s'\n", username)
}

func setPassword(username string) {
	findUserByUsername(username) // validate exists

	password := readPassword("New password: ")
	confirm := readPassword("Confirm password: ")
	if password != confirm {
		fmt.Fprintln(os.Stderr, "Error: passwords do not match")
		os.Exit(1)
	}

	hashedPw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	checkError(err)

	var user models.ModelPublicUsers
	err = storage.DB.NewSelect().Model(&user).Where("username = ?", username).Scan(context.Background())
	checkError(err)

	_, err = storage.DB.NewUpdate().Model(&user).
		Set("password = ?", string(hashedPw)).
		Set("updated_at = ?", time.Now()).
		Where("id = ?", user.ID.String()).
		Exec(context.Background())
	checkError(err)

	fmt.Printf("Password updated for '%s'\n", username)
}

func setUserActive(username string, active bool) {
	user := findUserByUsername(username)

	_, err := storage.DB.NewUpdate().Model(user).
		Set("active = ?", active).
		Set("updated_at = ?", time.Now()).
		Where("id = ?", user.ID.String()).
		Exec(context.Background())
	checkError(err)

	state := "enabled"
	if !active {
		state = "disabled"
	}
	fmt.Printf("User '%s' %s\n", username, state)
}

func updateUser(username string) {
	user := findUserByUsername(username)

	// If any flag was provided, apply flags only (non-interactive)
	flagsProvided := updateUsername != "" || updateEmail != "" || updateFullName != "" || updateRole != ""

	if flagsProvided {
		if updateUsername != "" {
			user.Username = resolvespec_common.NewSqlString(updateUsername)
		}
		if updateEmail != "" {
			user.Email = resolvespec_common.NewSqlString(updateEmail)
		}
		if updateFullName != "" {
			user.FullName = resolvespec_common.NewSqlString(updateFullName)
		}
		if updateRole != "" {
			user.Role = resolvespec_common.NewSqlString(updateRole)
		}
	} else {
		fmt.Printf("Updating user '%s' (press Enter to keep current value)\n", username)

		if v := promptLine("Username", user.Username.String()); v != "" {
			user.Username = resolvespec_common.NewSqlString(v)
		}
		if v := promptLine("Email", user.Email.String()); v != "" {
			user.Email = resolvespec_common.NewSqlString(v)
		}
		if v := promptLine("Full Name", user.FullName.String()); v != "" {
			user.FullName = resolvespec_common.NewSqlString(v)
		}
		if v := promptLine("Role (admin/user)", user.Role.String()); v != "" {
			user.Role = resolvespec_common.NewSqlString(v)
		}
	}

	_, err := storage.DB.NewUpdate().Model(user).
		Set("username = ?", user.Username.String()).
		Set("email = ?", user.Email.String()).
		Set("full_name = ?", user.FullName.String()).
		Set("role = ?", user.Role.String()).
		Set("updated_at = ?", time.Now()).
		Where("id = ?", user.ID.String()).
		Exec(context.Background())
	checkError(err)

	fmt.Printf("User '%s' updated\n", user.Username.String())
}

func deleteUser(username string) {
	user := findUserByUsername(username)

	fmt.Printf("Delete user '%s'? This cannot be undone. [y/N]: ", username)
	reader := bufio.NewReader(os.Stdin)
	line, _ := reader.ReadString('\n')
	if strings.ToLower(strings.TrimSpace(line)) != "y" {
		fmt.Println("Cancelled")
		return
	}

	_, err := storage.DB.NewDelete().Model(user).Where("id = ?", user.ID.String()).Exec(context.Background())
	checkError(err)

	fmt.Printf("User '%s' deleted\n", username)
}

func findUserByUsername(username string) *models.ModelPublicUsers {
	var user models.ModelPublicUsers
	err := storage.DB.NewSelect().Model(&user).Where("username = ?", username).Scan(context.Background())
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: user '%s' not found\n", username)
		os.Exit(1)
	}
	return &user
}

// promptLine prints a prompt and reads one line. Returns defaultVal if Enter pressed with no input.
func promptLine(label, defaultVal string) string {
	if defaultVal != "" {
		fmt.Printf("%s [%s]: ", label, defaultVal)
	} else {
		fmt.Printf("%s: ", label)
	}
	reader := bufio.NewReader(os.Stdin)
	line, err := reader.ReadString('\n')
	if err != nil && err != io.EOF {
		return defaultVal
	}
	v := strings.TrimRight(line, "\r\n")
	if v == "" {
		return defaultVal
	}
	return v
}

// promptRequired loops until a non-empty value is entered.
func promptRequired(label string) string {
	for {
		v := promptLine(label, "")
		if v != "" {
			return v
		}
		fmt.Printf("%s is required, please try again.\n", label)
	}
}

// readPassword reads a password with hidden input when on a TTY, plain text otherwise.
// Loops until a non-empty value is entered.
func readPassword(prompt string) string {
	fd := int(os.Stdin.Fd())
	isTTY := term.IsTerminal(fd)
	for {
		fmt.Print(prompt)
		var pw string
		if isTTY {
			p, err := term.ReadPassword(fd)
			fmt.Println()
			if err == nil {
				pw = string(p)
			}
		} else {
			reader := bufio.NewReader(os.Stdin)
			p, err := reader.ReadString('\n')
			fmt.Println()
			if err != nil && err != io.EOF {
				fmt.Fprintln(os.Stderr, "Error: cannot read password interactively. Use set-password <username> <password> instead.")
				os.Exit(1)
			}
			pw = strings.TrimRight(p, "\r\n")
		}
		if pw != "" {
			return pw
		}
		fmt.Println("Password cannot be empty, please try again.")
	}
}
