package processing import ( "context" "fmt" "github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts" "github.com/descrybe/descrybe-v2/apps/api/internal/catalog" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // FixCompanyCatalogOpts controls optional Fix A1 / catalog hygiene steps. type FixCompanyCatalogOpts struct { // BackfillCategories copies mapped_data category unique_ids onto empty // processed_products.category. BackfillCategories bool // ReprocessSampleLimit caps recommended raw_product_ids in the response // (0 = counts only, no sample ids). ReprocessSampleLimit int // AIPrompts optionally re-applies BuiltInDefaults product_enhance per content language. AIPrompts *aiprompts.Service // WPCategoriesSQL is an uploaded wp_product_categories.sql dump. When set, // RepairCompanyCategoryEnhancePrompts force-applies overlays from these bytes // (Admin Sync A1 primary path). Empty keeps filesystem auto-detect fallback. WPCategoriesSQL []byte } // FixCompanyCatalogResult is the idempotent admin Fix A1 report. type FixCompanyCatalogResult struct { CompanyID uuid.UUID `json:"company_id"` CompanyName string `json:"company_name"` A1Cohort bool `json:"a1_cohort"` CategoryAttributeOrphansRemoved int `json:"category_attribute_orphans_removed"` CategoryAttributeLinks int `json:"category_attribute_links"` CategoryPromptsUpdated int `json:"category_prompts_updated"` CategoryPromptsAlreadyOK int `json:"category_prompts_already_ok"` CategoryPromptsEmpty int `json:"category_prompts_empty_skipped"` // Prompts aliases category_prompts_updated for flash.admin.fixA1Success {prompts}. Prompts int `json:"prompts"` // WPCategoriesPath is set when prompts came from wp_product_categories.sql // (upload:… or a resolved filesystem path). WPCategoriesPath string `json:"wp_categories_path,omitempty"` WPCategoriesEntries int `json:"wp_categories_entries,omitempty"` ProductEnhanceLanguages int `json:"product_enhance_languages"` WeakHashesCleared int `json:"weak_hashes_cleared"` CategoriesBackfilled int `json:"categories_backfilled"` // MappedCategoriesBackfilled is processed → mapped_data.category copies. MappedCategoriesBackfilled int `json:"mapped_categories_backfilled"` // Coverage after Fix (Products UI reads mapped_data.category for raw rows). TaxonomyCategories int64 `json:"taxonomy_categories"` MappedWithCategory int64 `json:"mapped_with_category"` MappedWithoutCategory int64 `json:"mapped_without_category"` ProcessedWithCategory int64 `json:"processed_with_category"` ProcessedWithoutCategory int64 `json:"processed_without_category"` // Hashes / Categories alias the long names for flash.admin.fixA1Success. Hashes int `json:"hashes"` Categories int `json:"categories"` // MappedBackfilled aliases mapped_categories_backfilled for flash {mapped_backfilled}. MappedBackfilled int `json:"mapped_backfilled"` DescriptionsNormalized int `json:"descriptions_normalized"` DescriptionsBackfilled int `json:"descriptions_backfilled"` MappedDescriptionsFlat int `json:"mapped_descriptions_flattened"` MetaBackfilled int `json:"meta_backfilled"` AttributesSanitized int64 `json:"attributes_sanitized"` ProductsScanned int `json:"products_scanned"` ReprocessNeededCount int `json:"reprocess_needed_count"` ReprocessSampleRawProductIDs []uuid.UUID `json:"reprocess_sample_raw_product_ids,omitempty"` } // FixCompanyCatalog runs the admin Fix A1 / catalog hygiene sequence in place. // Never clears the catalog or uses A1 as a clone destination. // Step 2 applies RepairCompanyCategoryEnhancePrompts (same repaired map as // RepairA1DemoCategoryEnhancePrompts / cmd/repair-category-prompts -apply). // Legacy converters + weak-hash clear live in FixCatalogHygieneWithIDs. func FixCompanyCatalog(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID, companyName string, a1Cohort bool, opts FixCompanyCatalogOpts) (FixCompanyCatalogResult, error) { out := FixCompanyCatalogResult{ CompanyID: companyID, CompanyName: companyName, A1Cohort: a1Cohort, } if pool == nil { return out, fmt.Errorf("nil pool") } orphans, links, err := catalog.EnsureCategoryAttributeLinks(ctx, pool, companyID) if err != nil { return out, err } out.CategoryAttributeOrphansRemoved = orphans out.CategoryAttributeLinks = links updatedRes, err := catalog.RepairCompanyCategoryEnhancePromptsWithOptions(ctx, pool, companyID, catalog.RepairA1DemoOptions{ WPCategoriesSQL: opts.WPCategoriesSQL, }) if err != nil { return out, err } out.CategoryPromptsUpdated = updatedRes.Updated out.CategoryPromptsAlreadyOK = updatedRes.AlreadyOK out.CategoryPromptsEmpty = updatedRes.EmptySkipped out.WPCategoriesPath = updatedRes.WPCategoriesPath out.WPCategoriesEntries = updatedRes.SeedEntries if opts.AIPrompts != nil { n, err := opts.AIPrompts.ApplyBuiltInProductEnhance(ctx, companyID) if err != nil { return out, fmt.Errorf("product_enhance templates: %w", err) } out.ProductEnhanceLanguages = n } hygiene, clearedIDs, err := FixCatalogHygieneWithIDs(ctx, pool, companyID, opts.BackfillCategories) if err != nil { return out, err } out.WeakHashesCleared = hygiene.WeakHashesCleared out.CategoriesBackfilled = hygiene.CategoriesBackfilled out.DescriptionsNormalized = hygiene.DescriptionsNormalized out.DescriptionsBackfilled = hygiene.DescriptionsBackfilled out.MappedDescriptionsFlat = hygiene.MappedDescriptionsFlat out.MetaBackfilled = hygiene.MetaBackfilled out.ProductsScanned = hygiene.ProductsScanned if opts.BackfillCategories { mappedN, err := BackfillMappedCategoriesFromProcessed(ctx, pool, companyID) if err != nil { return out, fmt.Errorf("mapped category backfill: %w", err) } out.MappedCategoriesBackfilled = int(mappedN) } sanitized, err := BackfillCompanyProcessedAttributes(ctx, pool, companyID) if err != nil { return out, fmt.Errorf("sanitize attributes: %w", err) } out.AttributesSanitized = sanitized needed, sample, err := RecommendReprocessRawProductIDs(ctx, pool, companyID, clearedIDs, opts.ReprocessSampleLimit) if err != nil { return out, err } out.ReprocessNeededCount = needed out.ReprocessSampleRawProductIDs = sample mWith, mWithout, taxonomy, err := CountMappedCategoryCoverage(ctx, pool, companyID) if err != nil { return out, err } out.MappedWithCategory = mWith out.MappedWithoutCategory = mWithout out.TaxonomyCategories = taxonomy pWith, pWithout, err := CountProcessedCategoryCoverage(ctx, pool, companyID) if err != nil { return out, err } out.ProcessedWithCategory = pWith out.ProcessedWithoutCategory = pWithout // Short keys for flash.admin.fixA1Success {prompts,hashes,categories,mapped_backfilled}. out.Prompts = out.CategoryPromptsUpdated out.Hashes = out.WeakHashesCleared out.Categories = out.CategoriesBackfilled out.MappedBackfilled = out.MappedCategoriesBackfilled return out, nil }