// Package seed provides the embedded permission catalog and default
// system role set used by cmd/permission-seed and the test fixture.
package seed

import (
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"sort"
	"time"

	"gateway/internal/model/permission/domain/entity"
	"gateway/internal/model/permission/domain/enum"
	domrepo "gateway/internal/model/permission/domain/repository"

	"go.mongodb.org/mongo-driver/v2/bson"
)

//go:embed catalog.json
var catalogJSON []byte

// CatalogEntry mirrors the JSON shape on disk. Parent / Description are
// optional; HTTPMethods + HTTPPath empty marks a category node.
type CatalogEntry struct {
	Name        string `json:"name"`
	Parent      string `json:"parent,omitempty"`
	HTTPMethods string `json:"http_methods,omitempty"`
	HTTPPath    string `json:"http_path,omitempty"`
	Type        string `json:"type"`
	Status      string `json:"status,omitempty"`
	Description string `json:"description,omitempty"`
}

// LoadCatalog returns the embedded catalog as parsed entries.
func LoadCatalog() ([]*CatalogEntry, error) {
	var entries []*CatalogEntry
	if err := json.Unmarshal(catalogJSON, &entries); err != nil {
		return nil, fmt.Errorf("permission seed: parse catalog: %w", err)
	}
	return entries, nil
}

// SystemRoleDefinition is a default role seeded for every B2B tenant on
// creation. PermissionNames are catalog entries by Name; the seeder
// resolves them to IDs at apply-time.
type SystemRoleDefinition struct {
	Key             string
	DisplayName     string
	PermissionNames []string
}

// DefaultSystemRoles is the canonical set assigned to every new tenant
// per design §6.5. tenant_owner is undeletable; the rest can be edited.
var DefaultSystemRoles = []SystemRoleDefinition{
	{
		Key:         "tenant_owner",
		DisplayName: "Tenant Owner",
		PermissionNames: []string{
			"member.info.management",
			"permission.role.management",
			"system.management",
		},
	},
	{
		Key:         "tenant_admin",
		DisplayName: "Tenant Admin",
		PermissionNames: []string{
			"member.admin.list", "member.admin.read", "member.admin.update", "member.admin.status",
			"permission.role.read", "permission.role.write", "permission.role.modify",
			"permission.assign.write", "permission.assign.revoke",
			"permission.mapping.write", "permission.policy.reload",
		},
	},
	{
		Key:         "member_manager",
		DisplayName: "Member Manager",
		PermissionNames: []string{
			"member.admin.list", "member.admin.read", "member.admin.update", "member.admin.status",
		},
	},
	{
		Key:         "member",
		DisplayName: "Member",
		PermissionNames: []string{
			"member.info.select",
			"member.info.update",
		},
	},
	{
		Key:         "viewer",
		DisplayName: "Viewer",
		PermissionNames: []string{
			"member.info.select",
			"permission.role.read",
		},
	},
}

// ApplyOptions tunes the seeder.
type ApplyOptions struct {
	// TenantIDs receive the DefaultSystemRoles. Empty disables role seeding.
	TenantIDs []string
	// SkipCatalog skips the platform-wide upsert (for "tenant only" runs).
	SkipCatalog bool
}

// Apply upserts the catalog and (optionally) the default system roles for
// the supplied tenants. Idempotent: re-running only updates fields that
// changed.
func Apply(
	ctx context.Context,
	perms domrepo.PermissionRepository,
	roles domrepo.RoleRepository,
	rolePerms domrepo.RolePermissionRepository,
	opts ApplyOptions,
) (*Report, error) {
	report := &Report{}
	entries, err := LoadCatalog()
	if err != nil {
		return nil, err
	}

	if !opts.SkipCatalog {
		if err := upsertCatalog(ctx, perms, entries, report); err != nil {
			return nil, err
		}
	}

	if len(opts.TenantIDs) == 0 {
		return report, nil
	}

	allPerms, permByName, err := loadCatalogIndex(ctx, perms)
	if err != nil {
		return nil, err
	}
	for _, tenantID := range opts.TenantIDs {
		if err := seedTenantRoles(ctx, roles, rolePerms, tenantID, allPerms, permByName, report); err != nil {
			return nil, err
		}
	}
	return report, nil
}

// Report holds counters returned by Apply for CLI logging.
type Report struct {
	CatalogUpserted   int
	RolesUpserted     int
	RolePermissionSet int
}

func upsertCatalog(
	ctx context.Context,
	perms domrepo.PermissionRepository,
	entries []*CatalogEntry,
	report *Report,
) error {
	now := time.Now().UTC().UnixMilli()

	// First pass: name → parent name. Second pass: resolve parent name to
	// ID after every entry is upserted.
	parentByName := make(map[string]string, len(entries))
	for _, entry := range entries {
		parentByName[entry.Name] = entry.Parent
	}

	for _, entry := range entries {
		permType := enum.PermissionType(entry.Type)
		if permType == "" {
			permType = enum.PermissionTypeBackendUser
		}
		status := enum.Status(entry.Status)
		if status == "" {
			status = enum.StatusOpen
		}
		perm := &entity.Permission{
			Name:        entry.Name,
			HTTPMethods: entry.HTTPMethods,
			HTTPPath:    entry.HTTPPath,
			Status:      status,
			Type:        permType,
			UpdateAt:    now,
		}
		if err := perms.UpsertByName(ctx, perm); err != nil {
			return fmt.Errorf("permission seed: upsert %s: %w", entry.Name, err)
		}
		report.CatalogUpserted++
	}

	idByName, err := loadCatalogIDIndex(ctx, perms)
	if err != nil {
		return err
	}
	for _, entry := range entries {
		parentName, ok := parentByName[entry.Name]
		if !ok || parentName == "" {
			continue
		}
		parentID, ok := idByName[parentName]
		if !ok {
			return fmt.Errorf("permission seed: parent %q for %q not found", parentName, entry.Name)
		}
		perm := &entity.Permission{
			Name:        entry.Name,
			Parent:      parentID,
			HTTPMethods: entry.HTTPMethods,
			HTTPPath:    entry.HTTPPath,
			Status:      enum.Status(entry.Status),
			Type:        enum.PermissionType(entry.Type),
			UpdateAt:    now,
		}
		if perm.Status == "" {
			perm.Status = enum.StatusOpen
		}
		if perm.Type == "" {
			perm.Type = enum.PermissionTypeBackendUser
		}
		if err := perms.UpsertByName(ctx, perm); err != nil {
			return fmt.Errorf("permission seed: link parent %s: %w", entry.Name, err)
		}
	}
	return nil
}

func loadCatalogIDIndex(
	ctx context.Context,
	perms domrepo.PermissionRepository,
) (map[string]string, error) {
	all, byName, err := loadCatalogIndex(ctx, perms)
	if err != nil {
		return nil, err
	}
	idByName := make(map[string]string, len(all))
	for name, perm := range byName {
		idByName[name] = perm.ID.Hex()
	}
	return idByName, nil
}

func loadCatalogIndex(
	ctx context.Context,
	perms domrepo.PermissionRepository,
) ([]*entity.Permission, map[string]*entity.Permission, error) {
	all, err := perms.GetAll(ctx, nil)
	if err != nil {
		return nil, nil, err
	}
	sort.SliceStable(all, func(i, j int) bool {
		return all[i].Name < all[j].Name
	})
	byName := make(map[string]*entity.Permission, len(all))
	for _, p := range all {
		byName[p.Name] = p
	}
	return all, byName, nil
}

func seedTenantRoles(
	ctx context.Context,
	roles domrepo.RoleRepository,
	rolePerms domrepo.RolePermissionRepository,
	tenantID string,
	allPerms []*entity.Permission,
	permByName map[string]*entity.Permission,
	report *Report,
) error {
	for _, def := range DefaultSystemRoles {
		role, err := roles.GetByKey(ctx, tenantID, def.Key)
		if err != nil {
			role = &entity.Role{
				ID:          bson.NewObjectID(),
				TenantID:    tenantID,
				Key:         def.Key,
				DisplayName: def.DisplayName,
				Status:      enum.StatusOpen,
				IsSystem:    true,
			}
			if err := roles.Insert(ctx, role); err != nil {
				return fmt.Errorf("permission seed: tenant=%s create role %s: %w", tenantID, def.Key, err)
			}
			report.RolesUpserted++
		}
		permissionIDs, err := resolveRolePermissionIDs(def, allPerms, permByName)
		if err != nil {
			return err
		}
		if err := rolePerms.SetForRole(ctx, tenantID, role.ID.Hex(), permissionIDs); err != nil {
			return fmt.Errorf("permission seed: tenant=%s set role perms %s: %w", tenantID, def.Key, err)
		}
		report.RolePermissionSet += len(permissionIDs)
	}
	return nil
}

func resolveRolePermissionIDs(
	def SystemRoleDefinition,
	allPerms []*entity.Permission,
	permByName map[string]*entity.Permission,
) ([]string, error) {
	childrenByParent := make(map[string][]*entity.Permission, len(allPerms))
	parentByID := make(map[string]string, len(allPerms))
	for _, perm := range allPerms {
		id := perm.ID.Hex()
		parentByID[id] = perm.Parent
		childrenByParent[perm.Parent] = append(childrenByParent[perm.Parent], perm)
	}

	seen := make(map[string]struct{}, len(def.PermissionNames)*4)
	for _, name := range def.PermissionNames {
		perm, ok := permByName[name]
		if !ok {
			return nil, fmt.Errorf("permission seed: catalog missing %q for role %s", name, def.Key)
		}
		markWithParents(perm.ID.Hex(), parentByID, seen)
		if !perm.IsLeaf() {
			markDescendantLeaves(perm.ID.Hex(), childrenByParent, parentByID, seen)
		}
	}

	out := make([]string, 0, len(seen))
	for _, perm := range allPerms {
		id := perm.ID.Hex()
		if _, ok := seen[id]; ok {
			out = append(out, id)
		}
	}
	return out, nil
}

func markDescendantLeaves(
	parentID string,
	childrenByParent map[string][]*entity.Permission,
	parentByID map[string]string,
	seen map[string]struct{},
) {
	for _, child := range childrenByParent[parentID] {
		if child.IsLeaf() {
			markWithParents(child.ID.Hex(), parentByID, seen)
			continue
		}
		markDescendantLeaves(child.ID.Hex(), childrenByParent, parentByID, seen)
	}
}

func markWithParents(id string, parentByID map[string]string, seen map[string]struct{}) {
	for id != "" {
		if _, ok := seen[id]; ok {
			return
		}
		seen[id] = struct{}{}
		id = parentByID[id]
	}
}
