package applogic

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"aijianzhan/platform/internal/agentcap"
	"aijianzhan/platform/internal/agentstore"
	"aijianzhan/platform/internal/audit"
	"aijianzhan/platform/internal/authx"
	"aijianzhan/platform/internal/blueprint"
	"aijianzhan/platform/internal/meta"
	"aijianzhan/platform/internal/schema"
	"aijianzhan/platform/internal/svc"
	"aijianzhan/platform/internal/types"

	"github.com/google/uuid"
)

type PublishLogic struct {
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewPublishLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishLogic {
	return &PublishLogic{ctx: ctx, svcCtx: svcCtx}
}

func (l *PublishLogic) ListApps() (*types.AppListResp, error) {
	tenantID := authx.TenantID(l.ctx)
	items, err := l.svcCtx.Meta.ListByTenant(l.ctx, tenantID)
	if err != nil {
		return nil, err
	}
	allowed := map[string]struct{}{}
	filter := false
	scope := "all"
	// 管理账号：本租户全部模块（含在建）
	// 智能体：app_slugs 为空或含 * → 不限制；否则仅白名单
	if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil {
		acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, authx.AgentID(l.ctx))
		if err != nil {
			return nil, err
		}
		open := len(acc.AppSlugs) == 0
		for _, s := range acc.AppSlugs {
			if s == "*" {
				open = true
			}
			allowed[s] = struct{}{}
		}
		if open {
			scope = "open"
			filter = false
		} else {
			scope = "granted"
			filter = true
		}
	}
	out := make([]types.AppListItem, 0, len(items))
	for _, it := range items {
		if filter {
			if _, ok := allowed[it.Slug]; !ok {
				continue
			}
		}
		st := it.Status
		out = append(out, types.AppListItem{
			AppID:        it.AppID,
			Slug:         it.Slug,
			Name:         it.Name,
			Status:       string(st),
			StatusLabel:  meta.StatusLabelCN(st),
			Building:     meta.IsBuilding(st),
			SchemaName:   it.SchemaName,
			DatabaseName: it.DatabaseName,
			PageCount:    it.PageCount,
			EntityCount:  it.EntityCount,
			UpdatedAt:    it.UpdatedAt.UTC().Format(time.RFC3339),
			CreatedAt:    it.CreatedAt.UTC().Format(time.RFC3339),
		})
	}
	return &types.AppListResp{Items: out, Scope: scope}, nil
}

// SaveDraft 登记/更新「在建」模块蓝图（不跑 DDL）。管理账号可用来看到生成中尚未发布的模块。
func (l *PublishLogic) SaveDraft(slug string, req *types.DraftReq) (*types.AppListItem, error) {
	tenantID := authx.TenantID(l.ctx)
	bp, err := blueprint.Parse(req.Blueprint)
	if err != nil {
		return nil, err
	}
	slug = blueprint.NormalizeIdent(slug)
	bp.Meta.Slug = blueprint.NormalizeIdent(bp.Meta.Slug)
	if bp.Meta.Slug == "" {
		bp.Meta.Slug = slug
	}
	if slug == "" {
		slug = bp.Meta.Slug
	}
	bp.Meta.Slug = slug
	if bp.Apis.BasePath == "" || strings.Contains(bp.Apis.BasePath, "-") {
		bp.Apis.BasePath = "/api/v1/apps/" + slug
	}
	if bp.Meta.Name == "" {
		bp.Meta.Name = slug
	}
	if bp.Version == "" {
		bp.Version = "1.0"
	}
	if bp.Storage.Mode == "" {
		bp.Storage.Mode = "schema_per_app"
	}
	if bp.Storage.Engine == "" {
		bp.Storage.Engine = "postgres"
	}
	bp.EnsureDefaultImportExport()
	// 草稿允许尚未完全合法；尽量校验，失败则仍以宽松方式保存关键字段
	_ = bp.Validate(slug)

	now := time.Now().UTC()
	appID := uuid.NewString()
	schemaName := bp.AssignSchemaName(tenantID)
	if existing, err := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug); err == nil && existing != nil {
		if existing.Status == meta.StatusPublished {
			return nil, fmt.Errorf("module already published: %s (use publish to update pages)", slug)
		}
		appID = existing.AppID
		now = existing.CreatedAt
		if existing.SchemaName != "" {
			schemaName = existing.SchemaName
			bp.Storage.SchemaName = existing.SchemaName
		}
	}
	rec := &meta.AppRecord{
		AppID:      appID,
		TenantID:   tenantID,
		Slug:       slug,
		Name:       bp.Meta.Name,
		SchemaName: schemaName,
		Engine:     bp.Storage.Engine,
		Status:     meta.StatusDraft,
		Blueprint:  bp,
		CreatedAt:  now,
		UpdatedAt:  time.Now().UTC(),
	}
	if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
		return nil, err
	}
	_ = l.audit("draft.save", audit.DetailJSON(map[string]any{"slug": slug, "user_id": authx.UserID(l.ctx)}))
	return &types.AppListItem{
		AppID:       rec.AppID,
		Slug:        slug,
		Name:        rec.Name,
		Status:      string(meta.StatusDraft),
		StatusLabel: meta.StatusLabelCN(meta.StatusDraft),
		Building:    true,
		SchemaName:  schemaName,
		PageCount:   len(bp.Pages),
		EntityCount: len(bp.Entities),
		UpdatedAt:   rec.UpdatedAt.UTC().Format(time.RFC3339),
		CreatedAt:   rec.CreatedAt.UTC().Format(time.RFC3339),
	}, nil
}

func (l *PublishLogic) Publish(slug string, req *types.PublishReq) (*types.PublishResp, error) {
	tenantID := authx.TenantID(l.ctx)
	userID := authx.UserID(l.ctx)

	incoming, err := blueprint.Parse(req.Blueprint)
	if err != nil {
		return nil, err
	}
	slug = blueprint.NormalizeIdent(slug)
	incoming.Meta.Slug = blueprint.NormalizeIdent(incoming.Meta.Slug)
	if incoming.Meta.Slug == "" {
		incoming.Meta.Slug = slug
	}
	if slug == "" {
		slug = incoming.Meta.Slug
	}
	// 路径与蓝图不一致时以路径 slug 为准（先选应用再发布）
	if slug != "" && slug != incoming.Meta.Slug {
		incoming.Meta.Slug = slug
	}
	// Z39：API 地址只由本次发布路径 slug 决定，禁止克隆蓝图沿用旧模块 base_path。
	incoming.Apis.BasePath = "/api/v1/apps/" + slug

	mode := strings.ToLower(strings.TrimSpace(req.Mode))
	if mode == "" {
		mode = "auto"
	}
	if mode == "merge" {
		mode = "add_pages" // 兼容旧别名
	}
	switch mode {
	case "auto", "add_pages", "create", "replace":
	default:
		return nil, fmt.Errorf("unsupported publish mode: %s (use auto|add_pages|create|replace)", mode)
	}

	existing, existErr := l.svcCtx.Meta.GetBySlug(l.ctx, tenantID, slug)
	exists := existErr == nil && existing != nil

	publishMode := "created"
	var mergeRes *blueprint.MergeResult
	var bp *blueprint.Blueprint

	switch {
	case mode == "create":
		if exists {
			return nil, fmt.Errorf("app already exists: %s (select it and publish with mode=add_pages to add newly generated pages)", slug)
		}
		bp = incoming
		publishMode = "created"
	case mode == "add_pages":
		if !exists {
			return nil, fmt.Errorf("app not found: %s (create it first, or use mode=create/auto)", slug)
		}
		if existing.Blueprint == nil {
			return nil, fmt.Errorf("existing app has no blueprint")
		}
		bp = existing.Blueprint
		bp.Meta.Slug = slug
		bp.Apis.BasePath = "/api/v1/apps/" + slug
		mergeRes, err = blueprint.MergeInto(bp, incoming)
		if err != nil {
			if isNothingNewToPublish(err) {
				if needsSchemaIsolation(existing, authx.Role(l.ctx)) {
					publishMode = "storage_isolated"
					mergeRes = nil
				} else {
					return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
				}
			} else {
				return nil, err
			}
		} else {
			publishMode = "pages_added"
		}
	case mode == "replace":
		bp = incoming
		if exists {
			publishMode = "replaced"
		} else {
			publishMode = "created"
		}
	default: // auto
		if exists {
			if existing.Blueprint == nil {
				return nil, fmt.Errorf("existing app has no blueprint")
			}
			bp = existing.Blueprint
			bp.Meta.Slug = slug
			bp.Apis.BasePath = "/api/v1/apps/" + slug
			mergeRes, err = blueprint.MergeInto(bp, incoming)
			if err != nil {
				if isNothingNewToPublish(err) {
					if needsSchemaIsolation(existing, authx.Role(l.ctx)) {
						publishMode = "storage_isolated"
						mergeRes = nil
					} else {
						return l.handleNoBlueprintDelta(existing, req, slug, tenantID, userID)
					}
				} else {
					return nil, err
				}
			} else {
				publishMode = "pages_added"
			}
		} else {
			bp = incoming
			publishMode = "created"
		}
	}

	// Z36：有蓝图增量时也把 host_meta.module_name 落到蓝图/模块显示名
	if req.HostMeta != nil {
		if n := strings.TrimSpace(req.HostMeta.ModuleName); n != "" {
			bp.Meta.Name = n
		}
	}

	bp.EnsureDefaultImportExport()
	if err := bp.Validate(slug); err != nil {
		return nil, err
	}

	schemaName := bp.AssignSchemaName(tenantID)
	dbName := bp.AssignDatabaseName(tenantID)
	boundAgentDatabase := false
	// Z8c/Z39：智能体绑定库可由多个模块共享，因此库内仍须按 tenant+slug 分 Schema，
	// 不能把所有模块都放进 public 并按同名 entity table 复用。
	// Z35：中文昵称等非法名不得用于 EnsureDatabase，回落 AssignDatabaseName
	if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil {
		if aid := authx.AgentID(l.ctx); aid > 0 {
			if acc, err := l.svcCtx.Agents.Get(l.ctx, tenantID, aid); err == nil && acc != nil {
				if dn := strings.TrimSpace(acc.DatabaseName); dn != "" && schema.ValidDBName(dn) {
					bp.Storage.Mode = "schema_per_app"
					schemaName = bp.AssignSchemaName(tenantID)
					dbName = dn
					if exists && existing.DatabaseName != "" {
						dbName = existing.DatabaseName
					}
					boundAgentDatabase = true
				} else if dn != "" && !schema.ValidDBName(dn) {
					// 历史脏数据（如 DisplayName 当库名）：纠正智能体落点库名，避免反复 400
					safeName := schema.SanitizeDBName(tenantID, slug)
					_, _ = l.svcCtx.Agents.Update(l.ctx, tenantID, aid, agentstore.UpdateInput{
						DatabaseName: &safeName,
					})
					bp.Storage.Mode = "schema_per_app"
					schemaName = bp.AssignSchemaName(tenantID)
					dbName = safeName
					boundAgentDatabase = true
				} else if exists && existing.DatabaseName != "" {
					// 已发布模块仍位于智能体绑定库；重发时一并升级旧 public Schema。
					bp.Storage.Mode = "schema_per_app"
					schemaName = bp.AssignSchemaName(tenantID)
					dbName = existing.DatabaseName
					boundAgentDatabase = true
				}
			}
		}
	}
	ddl, err := schema.BuildPostgresDDL(bp)
	if err != nil {
		return nil, err
	}

	appID := uuid.NewString()
	now := time.Now().UTC()
	republish := false
	if exists {
		appID = existing.AppID
		now = existing.CreatedAt
		republish = existing.Status == meta.StatusPublished
		// Z39：旧版把智能体同库下的所有模块放入 public，导致同名资源串表。
		// 重发时切到独立 app_t{tenant}_{slug}；数据由既有双向同步重新灌入。
		if existing.SchemaName != "" && !(boundAgentDatabase && existing.SchemaName == "public") {
			schemaName = existing.SchemaName
			bp.Storage.SchemaName = existing.SchemaName
		}
		if existing.DatabaseName != "" && schema.ValidDBName(existing.DatabaseName) {
			dbName = existing.DatabaseName
		}
		ddl, err = schema.BuildPostgresDDL(bp)
		if err != nil {
			return nil, err
		}
	}
	rec := &meta.AppRecord{
		AppID:        appID,
		TenantID:     tenantID,
		Slug:         slug,
		Name:         bp.Meta.Name,
		SchemaName:   schemaName,
		DatabaseName: dbName,
		Engine:       bp.Storage.Engine,
		Status:       meta.StatusProvisioning,
		Blueprint:    bp,
		DDL:          ddl,
		CreatedAt:    now,
		UpdatedAt:    time.Now().UTC(),
	}
	if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
		return nil, fmt.Errorf("meta save: %w", err)
	}

	runner := l.svcCtx.Schema
	var appDB *sql.DB
	if dbName != "" {
		if err := runner.EnsureDatabase(l.ctx, dbName); err != nil {
			rec.Status = meta.StatusFailed
			rec.Error = err.Error()
			_ = l.svcCtx.Meta.Save(l.ctx, rec)
			_ = l.audit("publish.failed", audit.DetailJSON(map[string]any{"slug": slug, "error": err.Error()}))
			return nil, fmt.Errorf("ensure database: %w", err)
		}
		if l.svcCtx.Config.DataSource != "" {
			dsn, err := schema.DSNForDatabase(l.svcCtx.Config.DataSource, dbName)
			if err != nil {
				return nil, err
			}
			appDB, err = sql.Open("postgres", dsn)
			if err != nil {
				return nil, err
			}
			defer appDB.Close()
			runner = &schema.PostgresRunner{DB: appDB}
			if l.svcCtx.DBPool != nil {
				_, _ = l.svcCtx.DBPool.ForApp(rec)
			}
		}
	}

	// Z11a：蓝图新增字段 → ALTER TABLE ADD COLUMN IF NOT EXISTS
	inspectDB := appDB
	if inspectDB == nil {
		inspectDB = l.svcCtx.DB
	}
	if inspectDB != nil {
		alter, err := schema.BuildPostgresAlterDDL(l.ctx, inspectDB, bp)
		if err != nil {
			rec.Status = meta.StatusFailed
			rec.Error = err.Error()
			_ = l.svcCtx.Meta.Save(l.ctx, rec)
			return nil, fmt.Errorf("build alter ddl: %w", err)
		}
		if len(alter) > 0 {
			ddl = schema.MergeDDL(ddl, alter)
			rec.DDL = ddl
			_ = l.svcCtx.Meta.Save(l.ctx, rec)
		}
	}

	if err := runner.ExecDDL(l.ctx, ddl); err != nil {
		rec.Status = meta.StatusFailed
		rec.Error = err.Error()
		rec.UpdatedAt = time.Now().UTC()
		_ = l.svcCtx.Meta.Save(l.ctx, rec)
		_ = l.audit("publish.failed", audit.DetailJSON(map[string]any{"slug": slug, "error": err.Error()}))
		return nil, fmt.Errorf("provision failed: %w", err)
	}

	endpoints := buildEndpoints(bp)
	rec.Status = meta.StatusPublished
	rec.Endpoints = endpoints
	rec.Error = ""
	rec.UpdatedAt = time.Now().UTC()
	if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
		return nil, err
	}
	action := "publish.success"
	if republish {
		action = "publish.republish"
	}
	detail := map[string]any{
		"slug": slug, "schema": schemaName, "database": dbName, "user_id": userID,
		"republish": republish, "publish_mode": publishMode,
		"blueprint_revision": bp.Revision(), "resource_tables": resourceTables(bp),
	}
	if mergeRes != nil {
		detail["added_pages"] = mergeRes.AddedPages
		detail["added_entities"] = mergeRes.AddedEntities
	}
	_ = l.audit(action, audit.DetailJSON(detail))

	// 智能体新建发布：自动把该 slug 写入可访问模块，后续不必再去控制台授权
	if authx.Role(l.ctx) == authx.RoleAgent && l.svcCtx.Agents != nil && slug != "" {
		if aid := authx.AgentID(l.ctx); aid > 0 {
			_ = l.svcCtx.Agents.GrantAppSlug(l.ctx, aid, slug)
		}
	}

	ownerID := authx.AgentID(l.ctx)
	if ownerID <= 0 {
		ownerID = userID
	}
	secret := l.svcCtx.Config.Agent.CapsuleSecret
	if secret == "" {
		secret = l.svcCtx.JWT.AccessSecret
	}
	accessPath := ""
	accessURL := ""
	if secret != "" && ownerID > 0 {
		_, filePath, err := agentcap.SealModulePath(secret, tenantID, ownerID, slug)
		if err == nil {
			accessPath = filePath
			base := strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/")
			if req.HostMeta != nil && strings.TrimSpace(req.HostMeta.HostBaseURL) != "" {
				accessURL = strings.TrimRight(strings.TrimSpace(req.HostMeta.HostBaseURL), "/")
			} else if base != "" {
				accessURL = base + "/api/v1/public/" + filePath + "/blueprint"
			}
		}
	}

	moduleName := bp.Meta.Name
	publishStyle := ""
	if req.HostMeta != nil {
		if n := strings.TrimSpace(req.HostMeta.ModuleName); n != "" {
			moduleName = n
		}
		publishStyle = strings.TrimSpace(req.HostMeta.PublishStyle)
	}
	if publishStyle == "" {
		publishStyle = "immediate"
	}

	resp := &types.PublishResp{
		AppID:             rec.AppID,
		Slug:              slug,
		SchemaName:        schemaName,
		DatabaseName:      dbName,
		Status:            string(meta.StatusPublished),
		Endpoints:         endpoints,
		DDL:               ddl,
		MemoryMode:        l.svcCtx.MemoryMode,
		PublishMode:       publishMode,
		ModuleName:        moduleName,
		PublishStyle:      publishStyle,
		AccessPath:        accessPath,
		AccessURL:         accessURL,
		PublishedAt:       rec.UpdatedAt.UTC().Format(time.RFC3339),
		OwnerID:           ownerID,
		BlueprintRevision: bp.Revision(),
		ResourceTables:    resourceTables(bp),
	}
	if mergeRes != nil {
		resp.AddedPages = mergeRes.AddedPages
		resp.AddedEntities = mergeRes.AddedEntities
		resp.AddedResources = mergeRes.AddedResources
	}
	return resp, nil
}

func (l *PublishLogic) audit(action, detail string) error {
	if l.svcCtx.Audit == nil {
		return nil
	}
	return l.svcCtx.Audit.Log(l.ctx, authx.TenantID(l.ctx), authx.UserID(l.ctx), action, detail)
}

func isNothingNewToPublish(err error) bool {
	return err != nil && strings.Contains(err.Error(), "nothing new to publish")
}

func needsSchemaIsolation(existing *meta.AppRecord, role string) bool {
	return role == authx.RoleAgent &&
		existing != nil &&
		existing.DatabaseName != "" &&
		existing.SchemaName == "public"
}

// handleNoBlueprintDelta Z36：merge 无增量时，有 module_name 则仅更新 host_meta；否则中文业务错误。
func (l *PublishLogic) handleNoBlueprintDelta(
	existing *meta.AppRecord,
	req *types.PublishReq,
	slug string,
	tenantID, userID int64,
) (*types.PublishResp, error) {
	name := ""
	if req.HostMeta != nil {
		name = strings.TrimSpace(req.HostMeta.ModuleName)
	}
	if name == "" {
		return nil, fmt.Errorf("没有可发布的蓝图增量（页面/实体/接口未变化）。若仅修改模块显示名，请在 host_meta.module_name 传入新名称后重试 [NO_BLUEPRINT_DELTA]")
	}
	return l.publishHostMetaOnly(existing, req, slug, tenantID, userID, name)
}

// publishHostMetaOnly 无蓝图增量时只更新模块显示名并重签胶囊，不跑 DDL。
func (l *PublishLogic) publishHostMetaOnly(
	existing *meta.AppRecord,
	req *types.PublishReq,
	slug string,
	tenantID, userID int64,
	moduleName string,
) (*types.PublishResp, error) {
	if existing == nil || existing.Blueprint == nil {
		return nil, fmt.Errorf("existing app has no blueprint")
	}
	bp := existing.Blueprint
	bp.Meta.Name = moduleName
	bp.Meta.Slug = slug
	bp.Apis.BasePath = "/api/v1/apps/" + slug

	rec := existing
	rec.Name = moduleName
	rec.Blueprint = bp
	rec.UpdatedAt = time.Now().UTC()
	if rec.Status == "" {
		rec.Status = meta.StatusPublished
	}
	if err := l.svcCtx.Meta.Save(l.ctx, rec); err != nil {
		return nil, fmt.Errorf("meta save: %w", err)
	}
	_ = l.audit("publish.host_meta", audit.DetailJSON(map[string]any{
		"slug": slug, "module_name": moduleName, "user_id": userID, "publish_mode": "host_meta_updated",
	}))

	ownerID := authx.AgentID(l.ctx)
	if ownerID <= 0 {
		ownerID = userID
	}
	secret := l.svcCtx.Config.Agent.CapsuleSecret
	if secret == "" {
		secret = l.svcCtx.JWT.AccessSecret
	}
	accessPath := ""
	accessURL := ""
	if secret != "" && ownerID > 0 {
		_, filePath, err := agentcap.SealModulePath(secret, tenantID, ownerID, slug)
		if err == nil {
			accessPath = filePath
			base := strings.TrimRight(l.svcCtx.Config.PublicBaseURL, "/")
			if req.HostMeta != nil && strings.TrimSpace(req.HostMeta.HostBaseURL) != "" {
				accessURL = strings.TrimRight(strings.TrimSpace(req.HostMeta.HostBaseURL), "/")
			} else if base != "" {
				accessURL = base + "/api/v1/public/" + filePath + "/blueprint"
			}
		}
	}

	publishStyle := ""
	if req.HostMeta != nil {
		publishStyle = strings.TrimSpace(req.HostMeta.PublishStyle)
	}
	if publishStyle == "" {
		publishStyle = "immediate"
	}

	endpoints := rec.Endpoints
	if len(endpoints) == 0 {
		endpoints = buildEndpoints(bp)
	}
	return &types.PublishResp{
		AppID:             rec.AppID,
		Slug:              slug,
		SchemaName:        rec.SchemaName,
		DatabaseName:      rec.DatabaseName,
		Status:            string(rec.Status),
		Endpoints:         endpoints,
		MemoryMode:        l.svcCtx.MemoryMode,
		PublishMode:       "host_meta_updated",
		ModuleName:        moduleName,
		PublishStyle:      publishStyle,
		AccessPath:        accessPath,
		AccessURL:         accessURL,
		PublishedAt:       rec.UpdatedAt.UTC().Format(time.RFC3339),
		OwnerID:           ownerID,
		BlueprintRevision: bp.Revision(),
		ResourceTables:    resourceTables(bp),
	}, nil
}

func buildEndpoints(bp *blueprint.Blueprint) []string {
	base := strings.TrimRight(bp.Apis.BasePath, "/")
	if base == "" {
		base = "/api/v1/apps/" + bp.Meta.Slug
	}
	out := []string{"GET " + base + "/blueprint"}
	for _, r := range bp.Apis.Resources {
		path := r.Path
		if !strings.HasPrefix(path, "/") {
			path = "/" + path
		}
		full := base + path
		for _, op := range r.Operations {
			switch op {
			case "list":
				out = append(out, "GET "+full)
			case "create":
				out = append(out, "POST "+full)
			case "get":
				out = append(out, "GET "+full+"/{id}")
			case "update":
				out = append(out, "PUT "+full+"/{id}")
			case "delete":
				out = append(out, "DELETE "+full+"/{id}")
			case "import":
				out = append(out, "POST "+full+"/import")
			case "export":
				out = append(out, "GET "+full+"/export")
			}
		}
	}
	return out
}

func resourceTables(bp *blueprint.Blueprint) map[string]string {
	out := make(map[string]string, len(bp.Apis.Resources))
	entities := make(map[string]string, len(bp.Entities))
	for _, entity := range bp.Entities {
		entities[entity.Name] = entity.Table
	}
	for _, resource := range bp.Apis.Resources {
		path := strings.TrimPrefix(resource.Path, "/")
		if table := entities[resource.Entity]; path != "" && table != "" {
			out[path] = bp.Storage.SchemaName + "." + table
		}
	}
	return out
}
