// Package auth implements login/logout/me with opaque bearer tokens
// (Sanctum-parity), RBAC (role globs + admin bypass + per-user override).
package auth

import (
	"context"
	"encoding/json"
	"time"
)

// Role is the role string stored on users.role.
type Role string

const (
	RoleAdmin Role = "admin"
	RoleOwner Role = "owner"
	// Sales hierarchy (migration 000042): GM -> BM -> SPV -> Sales.
	RoleGM    Role = "gm"
	RoleBM    Role = "bm"
	RoleSPV   Role = "spv"
	RoleSales Role = "sales"
	// Retail outlet roles (migration 000044). The store hierarchy is a
	// SEPARATE axis from the B2B sales hierarchy above: a store manager runs
	// one outlet (its stock and its cashiers) and a cashier only sells at the
	// register. Before these existed, POS identities borrowed RoleSales, which
	// also carries masterdata.products.* and stock in/out — an outlet could
	// edit the central catalogue from a till.
	RoleStoreManager Role = "store_manager"
	RoleCashier      Role = "cashier"
	RoleBuyer        Role = "buyer"
	RoleFinance      Role = "finance"
	RoleFinanceHead  Role = "finance_head"
	RoleWarehouse    Role = "warehouse"
	RoleHRManager    Role = "hr_manager"
	RoleHRStaff      Role = "hr_staff"
	RoleEmployee     Role = "employee"
	RoleSystem       Role = "system"
)

// AllRoles lists every valid role (ENUM values on users.role).
var AllRoles = []Role{
	RoleAdmin, RoleOwner, RoleGM, RoleBM, RoleSPV, RoleSales,
	RoleStoreManager, RoleCashier, RoleBuyer,
	RoleFinance, RoleFinanceHead, RoleWarehouse, RoleHRManager, RoleHRStaff, RoleEmployee,
	RoleSystem,
}

// ApplicationAccess answers "which application may this identity enter?"
// This is a SEPARATE axis from Role/Permission (authz audit 2026-08-25):
// holding a backend account does NOT grant entry to every frontend. ERP,
// POS and E-Commerce are distinct applications; an internal ERP role must
// not reach the B2B storefront admin, and a B2B customer (buyer) must not
// reach ERP or POS. AppPlatform is the legacy unrestricted scope held only
// by platform-level roles (admin/owner); AppERP covers every internal
// operator role. AppECommerce = customer storefront; AppECommerceAdmin =
// B2B backoffice (gated by the customer's is_ecommerce_admin flag, not by
// the platform role).
type ApplicationAccess string

const (
	AppERP            ApplicationAccess = "erp"
	AppPOS            ApplicationAccess = "pos"
	AppECommerce      ApplicationAccess = "ecommerce"
	AppECommerceAdmin ApplicationAccess = "ecommerce_admin"
	AppPlatform       ApplicationAccess = "platform"
)

// AllApplications lists every distinct application context.
var AllApplications = []ApplicationAccess{
	AppERP, AppPOS, AppECommerce, AppECommerceAdmin, AppPlatform,
}

// DeriveApplicationAccess maps a stored role (plus the linked customer's
// e-commerce-admin flag) to the application(s) the identity may enter.
// AppPlatform (admin/owner) may enter any application; AppECommerceAdmin is
// derived only from the customer flag, never from the platform role, so an
// ERP admin can NEVER become a B2B admin by role alone. Every other internal
// role is ERP.
//
// LIVE-AUTHZ AUDIT 2026-08-26: a buyer whose customer row carries
// is_ecommerce_admin IS the B2B backoffice identity — previously this switch
// returned plain AppECommerce for them while GET /auth/me promoted the same
// user to ecommerce_admin, so the storefront UI showed an admin shell whose
// every API call was rejected by RequireApp(AppECommerceAdmin).
func DeriveApplicationAccess(role Role, isEcommerceAdmin bool) ApplicationAccess {
	switch role {
	case RoleAdmin, RoleOwner:
		return AppPlatform
	case RoleCashier:
		// A cashier exists only at the till. This is the first role that
		// derives to AppPOS on its own; before migration 000044 the value
		// could only be assigned by hand on users.application_access.
		return AppPOS
	case RoleBuyer:
		if isEcommerceAdmin {
			return AppECommerceAdmin
		}
		return AppECommerce
	default:
		if isEcommerceAdmin {
			return AppECommerceAdmin
		}
		return AppERP
	}
}

// AppScopes returns every application this identity may enter (used by
// frontends to pick the correct shell / landing page).
func (u *User) AppScopes() []ApplicationAccess {
	if u == nil {
		return nil
	}
	if u.ApplicationAccess == AppPlatform {
		return []ApplicationAccess{AppERP, AppPOS, AppECommerce, AppECommerceAdmin}
	}
	return []ApplicationAccess{u.ApplicationAccess}
}

// User mirrors the users row.
type User struct {
	ID                int64             `json:"id"`
	Name              string            `json:"name"`
	Email             string            `json:"email"`
	EmailVerifiedAt   *time.Time        `json:"email_verified_at"`
	Password          string            `json:"-"`
	Role              Role              `json:"role"`
	Permissions       Permissions       `json:"permissions"`
	ApplicationAccess ApplicationAccess `json:"application_access"`
	IsActive          bool              `json:"is_active"`
	LastLoginAt       *time.Time        `json:"last_login_at"`
	Avatar            *string           `json:"avatar"`
	CreatedAt         time.Time         `json:"created_at"`
	UpdatedAt         time.Time         `json:"updated_at"`
	WarehouseID       *int64            `json:"warehouse_id,omitempty"`
}

// Permissions is the per-user JSON override from users.permissions.
// Nil means "no override" → the role globs apply unchanged.
type Permissions map[string]any

// EffectivePermissions resolves the final permission map:
// role globs expanded, then overridden by the per-user permissions JSON.
// Returns a plain map of permission-name → true.
func (u *User) EffectivePermissions() map[string]bool {
	m := expandGlobs(globsFor(u.Role))
	// admin and owner bypass everything server-side (see Can); publish a
	// wildcard so frontends gate the same way. Without it the DB matrix
	// (admin listed with no globs = deny by omission) yields an empty set
	// while the server allows all — frontends then deny every gated route.
	if u.Role == RoleAdmin || u.Role == RoleOwner {
		m["*"] = true
	}
	if u.Permissions != nil {
		// Per-user override: explicit true grants, explicit false denies
		// (removed from the published set). Deny wins over role globs.
		for k, v := range u.Permissions {
			if b, ok := v.(bool); ok {
				if b {
					m[k] = true
					for ck := range catalogKeys {
						if globMatch(k, ck) {
							m[ck] = true
						}
					}
				} else {
					delete(m, k)
					for ck := range catalogKeys {
						if globMatch(k, ck) {
							delete(m, ck)
						}
					}
				}
			} else {
				delete(m, k)
			}
		}
	}
	return m
}

// Can reports whether the user holds a permission exactly or via a glob.
// admin and owner bypass everything (business-rules §7.3-7.4; owner == admin).
func (u *User) Can(perm string) bool {
	if u.Role == RoleAdmin || u.Role == RoleOwner {
		return true
	}
	if u.Permissions != nil {
		if b, ok := boolPerm(u.Permissions[perm]); ok {
			return b
		}
		// Wildcard deny in the override (e.g. "sales.*": false) must also
		// win over role globs. Grant stays exact-only: a glob-shaped true
		// must not silently widen the role.
		for k, v := range u.Permissions {
			if b, ok := boolPerm(v); ok && !b && globMatch(string(k), perm) {
				return false
			}
		}
	}
	return matchesAnyGlob(globsFor(u.Role), perm)
}

// CanAccessApp reports whether this identity may enter the given application.
// AppPlatform may enter any application (legacy unrestricted scope). An
// e-commerce admin may also enter the customer storefront (admin operates the
// storefront too), but a storefront customer may NOT enter the admin. Every
// other identity is confined to its own derived application context.
func (u *User) CanAccessApp(app ApplicationAccess) bool {
	if u == nil {
		return false
	}
	if u.ApplicationAccess == AppPlatform {
		return true
	}
	if u.ApplicationAccess == AppECommerceAdmin && app == AppECommerce {
		return true
	}
	return u.ApplicationAccess == app
}

// --- context helpers --------------------------------------------------------

type ctxKey int

const (
	userKey ctxKey = iota
	tokenKey
)

// ToContext stores the authenticated user + token record ID.
func ToContext(ctx context.Context, u *User, tokenID int64) context.Context {
	ctx = context.WithValue(ctx, userKey, u)
	ctx = context.WithValue(ctx, tokenKey, tokenID)
	return ctx
}

// UserFrom returns the authenticated user, or nil.
func UserFrom(ctx context.Context) *User {
	u, _ := ctx.Value(userKey).(*User)
	return u
}

// TokenIDFrom returns the token record id for the current session, or 0.
func TokenIDFrom(ctx context.Context) int64 {
	id, _ := ctx.Value(tokenKey).(int64)
	return id
}

func boolPerm(v any) (bool, bool) {
	b, ok := v.(bool)
	return b, ok
}

func expandGlobs(globs []string) map[string]bool {
	m := make(map[string]bool)
	for _, g := range globs {
		m[g] = true
		for ck := range catalogKeys {
			if globMatch(g, ck) {
				m[ck] = true
			}
		}
	}
	return m
}

func matchesAnyGlob(globs []string, perm string) bool {
	for _, g := range globs {
		if globMatch(g, perm) {
			return true
		}
	}
	return false
}

func globMatch(pattern, s string) bool {
	if pattern == s || pattern == "*" {
		return true
	}
	if len(pattern) >= 2 && pattern[len(pattern)-2:] == ".*" {
		prefix := pattern[:len(pattern)-2]
		return len(s) > len(prefix) && s[:len(prefix)] == prefix && s[len(prefix)] == '.'
	}
	return false
}

// permissionStringsTypes is a helper for tests (keeps json import used).
var _ = json.Valid
