// Creates a supervisor and a cashier at an outlet, then proves both can sign in.
//
// Exists because outlet 1135 — the one the terminal ships pointed at — had no
// staff at all, so the till fell back to the three PINs compiled into the app.
// Real staff here are what retire those.
//
// PINs are generated rather than chosen, from crypto/rand, and printed once so
// they can be handed to the shop. They are deliberately not derived from
// anything guessable.
//
//	go run ./scratch/posstaffsetup plan  1087 1135
//	go run ./scratch/posstaffsetup apply 1087 1135
package main

import (
	"crypto/rand"
	"fmt"
	"log"
	"math/big"
	"os"
	"strconv"

	"nearle/models"
	"nearle/repositories"

	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// newPassword generates a password for a supervisor's till login.
//
// From crypto/rand and printed once, like the PINs. Deliberately not derived
// from the shop's name or id: a credential anybody could guess from the sign
// above the door is not a credential.
func newPassword() string {
	const alphabet = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
	out := make([]byte, 14)
	for i := range out {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
		if err != nil {
			log.Fatalf("generating a password: %v", err)
		}
		out[i] = alphabet[n.Int64()]
	}
	return string(out)
}

func main() {
	mode, tenantID, locationID := "plan", 1087, 1135
	if len(os.Args) > 1 {
		mode = os.Args[1]
	}
	if len(os.Args) > 3 {
		tenantID, _ = strconv.Atoi(os.Args[2])
		locationID, _ = strconv.Atoi(os.Args[3])
	}

	_ = godotenv.Load()
	dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"),
		os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"))
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
	if err != nil {
		log.Fatal(err)
	}
	repo := repositories.NewPosRepository(db)

	var locName string
	db.Raw(`SELECT COALESCE(locationname,'') FROM tenantlocations WHERE locationid=? AND tenantid=?`,
		locationID, tenantID).Scan(&locName)
	if locName == "" {
		log.Fatalf("tenant %d has no outlet %d", tenantID, locationID)
	}

	// The configid the shop's other accounts use, so a new cashier is visible
	// to the same portal as everybody else at that outlet.
	var configID int
	db.Raw(`SELECT COALESCE(configid,0) FROM app_users
	        WHERE tenantid=? AND COALESCE(configid,0) > 0
	        GROUP BY configid ORDER BY COUNT(*) DESC LIMIT 1`, tenantID).Scan(&configID)

	fmt.Printf("tenant %d, outlet %d (%s), configid %d\n\n", tenantID, locationID, locName, configID)

	existing, err := repo.ListPosUsers(tenantID, locationID, true)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("till users already at this outlet: %d\n", len(existing))
	for _, u := range existing {
		fmt.Printf("  %-6d %-22s %-12s pin=%s %s\n", u.Userid, u.Fullname, u.Role, u.Pin, u.Status)
	}
	if len(existing) > 0 {
		fmt.Println("\nAlready set up. Nothing to do — this refuses to add duplicates.")
		return
	}

	// The supervisor gets a username and password as well as a PIN, because a
	// PIN cannot open a *closed* terminal — the PIN route requires a session
	// that already exists. Without these, an outlet whose only accounts are POS
	// accounts has no way in at all: the back-office logins are refused by role
	// and the till logins have no password. That deadlock is not hypothetical;
	// it is what the first cut of strict mode actually produced.
	//
	// The cashier deliberately gets neither. They sign on at a terminal a
	// supervisor has already opened, so a second password would be one more
	// credential to leak for no capability gained.
	//
	// The username is derived from the outlet rather than from a person, so it
	// survives staff turnover. `authname` is not unique in this schema, but
	// scoping it to the outlet keeps it unambiguous in practice, and `email` is
	// left null on purpose — that column *is* unique, and blank strings collide.
	wanted := []models.PosUserRequest{
		{
			Fullname: "Store Supervisor",
			Role:     "supervisor",
			Pin:      newPin(),
			Authname: fmt.Sprintf("supervisor.%d@pos.nearle.in", locationID),
			Password: newPassword(),
		},
		{Fullname: "Counter Cashier", Role: "cashier", Pin: newPin()},
	}
	for wanted[0].Pin == wanted[1].Pin {
		wanted[1].Pin = newPin()
	}

	fmt.Println("\nwould create:")
	for _, w := range wanted {
		fmt.Printf("  %-22s %-12s pin=%s", w.Fullname, w.Role, w.Pin)
		if w.Authname != "" {
			fmt.Printf("  login=%s / %s", w.Authname, w.Password)
		}
		fmt.Println()
	}

	if mode != "apply" {
		fmt.Println("\nNothing written — run `apply` to commit.")
		return
	}

	fmt.Println()
	for _, w := range wanted {
		created, err := repo.CreatePosUser(tenantID, locationID, configID, w)
		if err != nil {
			log.Fatalf("creating %s: %v", w.Fullname, err)
		}
		fmt.Printf("  created userid %-6d %-22s %-12s PIN %s",
			created.Userid, created.Fullname, created.Role, created.Pin)
		if w.Authname != "" {
			fmt.Printf("  login=%s / %s", w.Authname, w.Password)
		}
		fmt.Println()
	}

	// The point of the exercise: does the till now see real staff?
	staff, err := repo.PosStaff(tenantID, locationID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("\n/pos/staff now returns %d person(s):\n", len(staff))
	for _, s := range staff {
		fmt.Printf("  %-22s %-12s\n", s.Fullname, s.Role)
	}

	// And can they actually sign in?
	fmt.Println("\nPIN sign-in:")
	for _, w := range wanted {
		session, err := repo.PosLoginByPin(tenantID, locationID, w.Pin)
		if err != nil {
			fmt.Printf("  %-22s REFUSED: %v\n", w.Fullname, err)
			continue
		}
		fmt.Printf("  %-22s -> %s at %s, can_manage_staff=%v\n",
			w.Fullname, session.Role, session.Locationname, session.Canmanagestaff)
	}

	if _, err := repo.PosLoginByPin(tenantID, locationID, "5555"); err != nil {
		fmt.Printf("\n  an unknown PIN is refused: %v\n", err)
	} else {
		fmt.Println("\n  !! an unknown PIN was ACCEPTED")
	}

	fmt.Println("\n-- undo:")
	fmt.Printf("UPDATE app_users SET status='InActive' WHERE tenantid=%d AND locationid=%d AND roleid IN (%d,%d);\n",
		tenantID, locationID, models.PosRoleSupervisor, models.PosRoleCashier)
}

// newPin returns a four-digit PIN this schema can store, from crypto/rand.
//
// 1000–9999 because a leading zero cannot survive a bigint column, and the
// obvious ones are rejected by validatePosPin anyway — retried here rather than
// filtered, so the distribution stays even.
func newPin() string {
	for {
		n, err := rand.Int(rand.Reader, big.NewInt(9000))
		if err != nil {
			log.Fatal(err)
		}
		pin := strconv.FormatInt(n.Int64()+1000, 10)
		switch pin {
		case "1234", "1111", "2345", "3456", "4321", "9999", "2222":
			continue
		}
		return pin
	}
}
