package auth

import "sort"

// Permission catalog: the ONE canonical registry of every enforced
// permission key. The database is the only assignment store
// (role_permissions rows + users.permissions overrides); this catalog is the
// validation set and the GET /permissions source. The frontend holds zero
// copies and renders this verbatim.
//
// Group labels and key order were moved once from the ERP frontend
// PERM_GROUPS (since deleted); the four masterdata *.read keys were appended
// inside their families and the Keranjang group added because seeded role
// rows grant them while the frontend never listed them — dropping them here
// would turn live seed rows into "unknown" keys.
//
// Per-key labels echo the key: the frontend never had per-key labels, so
// identity keeps today's rendered text unchanged without inventing wording.
type CatalogKey struct {
	Key   string `json:"key"`
	Label string `json:"label"`
}

type CatalogGroup struct {
	Label string       `json:"label"`
	Keys  []CatalogKey `json:"keys"`
}

// CatalogView is the GET /permissions payload.
type CatalogView struct {
	Groups []CatalogGroup `json:"groups"`
}

var catalogSpec = []struct {
	label string
	keys  []string
}{
	{"Sistem — Pengguna, Peran & Toko", []string{
		"system.users.view", "system.users.create", "system.users.update", "system.users.delete",
		"system.roles.view", "system.roles.create", "system.roles.update", "system.roles.delete",
		"system.activity-logs.view", "system.activity-logs.delete",
		"system.stores.view", "system.stores.create", "system.stores.update", "system.stores.delete",
		"system.settings.view", "system.settings.manage", "system.notifications.send",
		"system.reports.view", "system.reports.manage",
	}},
	{"Master Data", []string{
		"masterdata.products.read", "masterdata.products.create", "masterdata.products.update", "masterdata.products.delete",
		"masterdata.products.units.create", "masterdata.products.units.update", "masterdata.products.units.delete",
		"masterdata.products.barcodes.create", "masterdata.products.barcodes.delete",
		"masterdata.categories.read", "masterdata.categories.create", "masterdata.categories.update", "masterdata.categories.delete",
		"masterdata.units.read", "masterdata.units.create", "masterdata.units.update", "masterdata.units.delete",
		"masterdata.brands.read", "masterdata.brands.create", "masterdata.brands.update", "masterdata.brands.delete",
		"masterdata.customers.read", "masterdata.customers.create", "masterdata.customers.update", "masterdata.customers.delete",
		"masterdata.suppliers.view", "masterdata.suppliers.create", "masterdata.suppliers.update", "masterdata.suppliers.delete",
	}},
	{"Keanggotaan", []string{
		"masterdata.memberships.read", "masterdata.memberships.lookup", "masterdata.memberships.create",
		"masterdata.memberships.manage", "masterdata.memberships.tiers.manage",
	}},
	{"Inventory & Purchasing", []string{
		"inventory.stock.view", "inventory.movements.view", "inventory.stock-in.create", "inventory.stock-out.create",
		// inventory.cost.view is a COST-VISIBILITY key, deliberately separate
		// from inventory.stock.view, which is an AVAILABILITY key.
		//
		// GET /inventory/stock has one response shape carrying both, so no
		// permission key could express "you may see how much stock there is but
		// not what we paid for it". A POS operator needs the first to sell and a
		// B2B buyer needs the first to order; neither needs the second, and
		// purchase_price/hpp/value are the margin. This key is what internal/
		// inventory checks to decide whether to serve those three fields, and it
		// is granted by migration 000103 to exactly the roles that still hold
		// accounting.ledger.view afterwards, so cost visibility follows ledger
		// visibility rather than a second, separately-drifting policy.
		//
		// The fields are OMITTED for a caller without this key, never zeroed: a
		// zero is a real cost and a caller must be able to tell "you may not see
		// this" from "this was free".
		"inventory.cost.view",
		"inventory.opnames.view", "inventory.opnames.create", "inventory.opnames.update", "inventory.opnames.finalize",
		"inventory.transfers.view", "inventory.transfers.create",
		"inventory.warehouses.view", "inventory.warehouses.create", "inventory.warehouses.update", "inventory.warehouses.delete",
		"purchasing.purchase-orders.view", "purchasing.purchase-orders.create", "purchasing.purchase-orders.update",
		"purchasing.purchase-orders.delete", "purchasing.purchase-orders.approve", "purchasing.purchase-orders.cancel",
		"purchasing.purchase-orders.pay", "purchasing.debts.export",
		"purchasing.goods-receipts.view", "purchasing.goods-receipts.create",
		"purchasing.vouchers.view", "purchasing.vouchers.create", "purchasing.vouchers.update",
	}},
	{"Sales & Finance", []string{
		"sales.sales-orders.view", "sales.sales-orders.create", "sales.sales-orders.update",
		"sales.sales-orders.delete", "sales.sales-orders.confirm", "sales.sales-orders.cancel",
		"sales.sales-vouchers.view", "sales.sales-vouchers.create", "sales.sales-vouchers.update",
		"sales.sales-vouchers.delete", "sales.sales-vouchers.validate",
		"sales.targets.view", "sales.targets.manage",
		"fakturisasi.invoices.view", "fakturisasi.invoices.create", "fakturisasi.invoices.delete", "fakturisasi.invoices.pay",
		"fakturisasi.payments.view", "fakturisasi.payments.verify", "fakturisasi.receivables.export",
		"sales.lhpp.view", "sales.lhpp.pay",
		// sales.lhpp.merge is the manual grouping surface (merge / split / void /
		// change driver) added with the persisted lhpp_groups tables. It is a
		// separate key from view and pay on purpose: reading a sheet and paying
		// against it are billing actions, while re-batching DOs changes which
		// document a collector carries, so it needs its own grant.
		"sales.lhpp.merge",
		"finance.accounts.view", "finance.cash-accounts.view", "finance.cash-accounts.create",
		"finance.cashflows.view", "finance.cashflows.create", "finance.cashflows.update",
		"finance.cashflows.delete", "finance.cashflows.export",
		"finance.payment-vouchers.view", "finance.payment-vouchers.create",
		"finance.payment-vouchers.post", "finance.payment-vouchers.void",
	}},
	{"HR & Tracking", []string{
		"hr.employees.view", "hr.employees.create", "hr.employees.update", "hr.employees.delete",
		"hr.attendance.view", "hr.attendance.create",
		"hr.leaves.view", "hr.leaves.store", "hr.leaves.approve",
		// Filing leave FOR ANOTHER employee. Deliberately inside the
		// hr.leaves.* namespace so the existing role globs decide it with no
		// migration: hr_staff holds hr.leaves.* and gains it, employee holds
		// only hr.leaves.store and does not. See internal/hr leave_target.go.
		"hr.leaves.store.any",
		"hr.payrolls.view", "hr.payrolls.generate", "hr.payrolls.approve", "hr.payrolls.pay",
		"hr.performances.view", "hr.performances.create", "hr.performances.update", "hr.performances.delete",
		"hr.clockin", "hr.clockout",
		"sales_tracking.visits.view", "sales_tracking.visits.create",
		"sales_tracking.visits.create.billing", "sales_tracking.visits.create.routine",
		"sales_tracking.visits.view_own",
		"sales_tracking.visits.checkin", "sales_tracking.visits.checkout",
		"sales_tracking.activities.view", "sales_tracking.activities.create",
		"sales_tracking.locations.view", "sales_tracking.locations.ping",
		"sales_tracking.routes.view", "sales_tracking.routes.create",
	}},
	{"Kasir & Galon (POS)", []string{
		"sales.pos.create",
		"cashier.shifts.view", "cashier.shifts.open", "cashier.shifts.close", "cashier.shifts.drawer",
		"sales.returns.create", "sales.returns.view",
		"galon.transactions.view", "galon.transactions.create", "galon.transactions.complete",
		"galon.transactions.cancel", "galon.transactions.return",
	}},
	{"Akuntansi", []string{
		"accounting.accounts.view", "accounting.accounts.create", "accounting.accounts.update",
		"accounting.departments.view", "accounting.departments.create", "accounting.departments.update",
		"accounting.journals.view", "accounting.journals.create", "accounting.journals.update",
		"accounting.journals.delete", "accounting.journals.post", "accounting.journals.void",
		"accounting.journals.reverse", "accounting.ledger.view",
		"accounting.periods.view", "accounting.periods.preview", "accounting.periods.validate", "accounting.periods.close",
		"accounting.periods.reopen",
		"accounting.advances.view", "accounting.advances.create", "accounting.advances.allocate", "accounting.advances.refund",
	}},
	{"Distribusi", []string{
		"distribution.packing-orders.view", "distribution.packing-orders.update",
		"distribution.delivery-orders.view", "distribution.delivery-orders.create",
		"distribution.dispatches.create",
		"distribution.fleet.view", "distribution.fleet.create",
		"distribution.shipments.view", "distribution.shipments.create", "distribution.shipments.deliver",
		"distribution.tracking.lookup", "distribution.tracking.record",
		"distribution.warehouse.view", "distribution.warehouse.create", "distribution.warehouse.receive",
	}},
	{"Dasbor & Laporan", []string{
		"dashboard.view", "dashboard.pos.view", "dashboard.widgets.manage", "sales.reports.view", "finance.reports.view",
	}},
	{"Keranjang", []string{
		"cart.add", "cart.checkout",
	}},
	{"Layanan Bantuan & Tiket Kendala", []string{
		"support.tickets.view", "support.tickets.create", "support.tickets.manage",
	}},
}

var catalogKeys map[string]bool

func init() {
	catalogKeys = make(map[string]bool)
	for _, g := range catalogSpec {
		for _, k := range g.keys {
			catalogKeys[k] = true
		}
	}
}

// Catalog returns a deep copy of the grouped permission catalog.
func Catalog() CatalogView {
	groups := make([]CatalogGroup, 0, len(catalogSpec))
	for _, g := range catalogSpec {
		keys := make([]CatalogKey, 0, len(g.keys))
		for _, k := range g.keys {
			keys = append(keys, CatalogKey{Key: k, Label: k})
		}
		groups = append(groups, CatalogGroup{Label: g.label, Keys: keys})
	}
	return CatalogView{Groups: groups}
}

// IsKnownPermission reports whether key may be stored: an exact catalog key,
// or a glob covering at least one catalog key (so stored "sales.*"-style
// rows and wildcard-deny overrides keep working, while typos and dead rows
// are rejected). Never invent a third case: anything else is unknown.
func IsKnownPermission(key string) bool {
	if catalogKeys[key] {
		return true
	}
	for k := range catalogKeys {
		if globMatch(key, k) {
			return true
		}
	}
	return false
}

// FirstUnknownKey returns the first key no catalog entry covers, or "".
// Sorted so multi-key rejections name a deterministic culprit.
func FirstUnknownKey(keys []string) string {
	sorted := append([]string(nil), keys...)
	sort.Strings(sorted)
	for _, k := range sorted {
		if !IsKnownPermission(k) {
			return k
		}
	}
	return ""
}
