// Package system implements users/roles management and the audit trail
// (domain-map.md D11). Routes carry the system.* permission family; the admin
// role bypasses everything.
package system

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

	"github.com/jackc/pgx/v5/pgtype"

	"github.com/TaruLab/keluargamart-backend/internal/auth"
	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/shopspring/decimal"
)

// RoleInput is the create/update payload for a role.
type RoleInput struct {
	Name string `json:"name"`
}

// RoleResponse is one roles row plus the additive builtin flag (true iff the
// name is in auth.AllRoles, computed server-side).
type RoleResponse struct {
	ID        int64              `json:"id"`
	Name      string             `json:"name"`
	CreatedAt pgtype.Timestamptz `json:"created_at"`
	UpdatedAt pgtype.Timestamptz `json:"updated_at"`
	Builtin   bool               `json:"builtin"`
}

// RoleMatrixView is the GET /role-permissions payload: the stored rows plus
// the additive top-level effective allow-set per role (builtin globs expanded
// when the role has zero rows, else the row set).
type RoleMatrixView struct {
	Rows      []db.ListRolePermissionsRow `json:"rows"`
	Effective map[string]map[string]bool  `json:"effective"`
}

// StoreInput is the create/update payload for a tenant store. is_active
// defaults to true on create; nil on update leaves it unchanged.
//
// WarehouseID binds the outlet to its stock-keeping warehouse (every outlet
// owns exactly one — see migration 000045). Optional on create: when omitted
// the service provisions a fresh `WH-<code>` warehouse in the same
// transaction, so ERP's "Tambah Toko" form need never touch the catalogue's
// warehouse list. Ignored on update (re-pointing an outlet's warehouse would
// strand its stock and is not a management action we expose).
type StoreInput struct {
	Code        string           `json:"code"`
	Name        string           `json:"name"`
	Address     *string          `json:"address"`
	Phone       *string          `json:"phone"`
	IsActive    *bool            `json:"is_active"`
	WarehouseID *int64           `json:"warehouse_id"`
	Latitude    *decimal.Decimal `json:"latitude,omitempty"`
	Longitude   *decimal.Decimal `json:"longitude,omitempty"`
}

// RolePermissionInput grants or revokes one permission on a role.
// allow=false revokes; omitted defaults to true (grant).
type RolePermissionInput struct {
	Role        string   `json:"role"`
	Permission  string   `json:"permission"`
	Permissions []string `json:"permissions"`
	Allow       *bool    `json:"allow"`
}

// ActivityLog is the API response shape for one activity_logs row (the audit
// trail). Persistence uses db.ActivityLog; old/new_values are JSONB kept as
// RawMessage so they embed rather than base64-encode.
type ActivityLog struct {
	ID          int64   `json:"id"`
	UserID      *int64  `json:"user_id"`
	Module      *string `json:"module"`
	Action      *string `json:"action"`
	Description *string `json:"description"`
	ModelType   *string `json:"model_type"`
	ModelID     *int64  `json:"model_id"`
	// ObjectLabel names the affected object ("ezaak", "SO-202610-000042"),
	// resolved in batch at list time. Absent when the type is unknown or the
	// row was deleted since.
	ObjectLabel *string         `json:"object_label,omitempty"`
	OldValues   json.RawMessage `json:"old_values,omitempty"`
	NewValues   json.RawMessage `json:"new_values,omitempty"`
	IPAddress   *string         `json:"ip_address"`
	UserAgent   *string         `json:"user_agent"`
	CreatedAt   time.Time       `json:"created_at"`
	UpdatedAt   time.Time       `json:"updated_at"`
	// UserName is joined on the activity-logs list only.
	UserName string `json:"user_name,omitempty"`
}

// NotificationItem is the API response shape for live notifications.
type NotificationItem struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	Message   string    `json:"message"`
	Type      string    `json:"type"`
	IsUrgent  bool      `json:"is_urgent"`
	Link      string    `json:"link"`
	Time      string    `json:"time"`
	Read      bool      `json:"read"`
	CreatedAt time.Time `json:"created_at"`
}

// UserInput is the create/update payload for a managed user. Password is
// required on create (min 8 chars) and optional on update. is_active defaults
// to true when omitted on create; nil on update leaves it unchanged.
type UserInput struct {
	Name              string          `json:"name"`
	Email             string          `json:"email"`
	Password          string          `json:"password"`
	Role              string          `json:"role"`
	Permissions       map[string]bool `json:"permissions"`
	IsActive          *bool           `json:"is_active"`
	Module            string          `json:"module,omitempty"`
	ApplicationAccess string          `json:"application_access,omitempty"`
}

// UserFilter holds query parameters for filtering users.
type UserFilter struct {
	Search string
	Role   string
	Module string
	Status string
}

// ModuleCounts holds the number of users in each module.
type ModuleCounts struct {
	All int64 `json:"all"`
	ERP int64 `json:"erp"`
	POS int64 `json:"pos"`
	B2B int64 `json:"b2b"`
}

// LogQuery filters the activity log listing.
type LogQuery struct {
	Module   string
	UserID   int64
	DateFrom string
	DateTo   string
}

// Tx is a physical transaction; pgx.Tx satisfies it. It doubles as the
// db.DBTX handed to tx-scoped store writes.
type Tx interface {
	db.DBTX
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

// Store is the persistence dependency of Service. Implemented by Repository;
// faked in unit tests. Users surface as the auth.User domain type (shared RBAC
// shape); roles and logs use the sqlc-generated types.
type Store interface {
	// Begin opens a transaction for the write groups that must land together
	// (the role-permission revoke + wildcard remediation). Same seam as
	// internal/sales and internal/finance.
	Begin(ctx context.Context) (Tx, error)

	ListUsers(ctx context.Context, page, perPage int, filter UserFilter) ([]auth.User, int64, ModuleCounts, error)
	// ListUsersInStores scopes the user list to the given tenant store(s),
	// further limited to `roles` the caller may manage plus `selfID`. Used for a
	// store-bound caller (kepala toko). UserInStores checks one user.
	ListUsersInStores(ctx context.Context, storeIDs []int64, roles []string, selfID int64, page, perPage int, filter UserFilter) ([]auth.User, int64, ModuleCounts, error)
	UserInStores(ctx context.Context, userID int64, storeIDs []int64) (bool, error)
	UserByID(ctx context.Context, id int64) (*auth.User, error)
	UserByEmail(ctx context.Context, email string) (*auth.User, error)
	CountActiveAdmins(ctx context.Context) (int64, error)
	CreateUser(ctx context.Context, u *auth.User, passwordHash string) (*auth.User, error)
	UpdateUser(ctx context.Context, u *auth.User, passwordHash *string) (*auth.User, error)
	SetUserActive(ctx context.Context, id int64, active bool) error
	DeleteUser(ctx context.Context, id int64) error

	ListRoles(ctx context.Context) ([]db.Role, error)
	RoleByID(ctx context.Context, id int64) (db.Role, error)
	RoleByName(ctx context.Context, name string) (db.Role, error)
	CountUsersByRole(ctx context.Context, role string) (int64, error)
	CreateRole(ctx context.Context, name string) (db.Role, error)
	UpdateRole(ctx context.Context, id int64, name string) (db.Role, error)
	// RenameRole cascades roles.name + users.role + role_permissions.role in
	// ONE transaction.
	RenameRole(ctx context.Context, id int64, oldName, newName string) (db.Role, error)
	// DeleteRoleTx refuses with ErrInUse when any user still holds the role,
	// else deletes its role_permissions rows + role row in ONE transaction.
	DeleteRoleTx(ctx context.Context, id int64, name string) error
	DeleteRole(ctx context.Context, id int64) error

	ListStores(ctx context.Context) ([]db.Store, error)
	StoreByID(ctx context.Context, id int64) (db.Store, error)
	// CreateStoreWithWarehouse inserts the store and, when wh is non-nil, the
	// paired warehouse it will be bound to — both in one transaction. When wh
	// is nil, sp.WarehouseID must already reference an existing warehouse.
	CreateStoreWithWarehouse(ctx context.Context, sp db.CreateStoreParams, wh *db.CreateWarehouseParams) (db.Store, error)
	UpdateStore(ctx context.Context, p db.UpdateStoreParams) (db.Store, error)
	DeleteStore(ctx context.Context, id int64) error
	CountStoresByWarehouse(ctx context.Context, warehouseID int64) (int64, error)
	WarehouseHasInventory(ctx context.Context, warehouseID int64) (bool, error)
	DeactivateWarehouse(ctx context.Context, id int64) error
	DeleteWarehouse(ctx context.Context, id int64) error

	ListStoreUsers(ctx context.Context, storeID int64) ([]db.ListStoreUsersRow, error)
	AddStoreUser(ctx context.Context, storeID, userID int64) error
	RemoveStoreUser(ctx context.Context, storeID, userID int64) (int64, error)

	// The three role-permission accessors take a db.DBTX so one revoke, its
	// wildcard remediation and the wildcard delete share a single transaction
	// boundary; nil means "use the pool" (read paths, main.go's boot load).
	RolePermissionMatrix(ctx context.Context, q db.DBTX) ([]db.ListRolePermissionsRow, error)
	UpsertRolePermission(ctx context.Context, q db.DBTX, role, permission string) error
	DeleteRolePermission(ctx context.Context, q db.DBTX, role, permission string) (int64, error)

	ListLogs(ctx context.Context, q LogQuery, page, perPage int) ([]db.ListActivityLogsRow, int64, error)
	InsertLog(ctx context.Context, p db.CreateActivityLogParams) error
	ObjectLabels(ctx context.Context, modelType string, ids []int64) (map[int64]string, error)
	ClearLogs(ctx context.Context) error
}
