package repositories

import (
	"errors"
	"fmt"
	"nearle/db"
	"nearle/models"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"
)

// catalogueBrandTables is the allowlist mapping a brand query param to its
// table name in the catalogue DB. Table names cannot be parameterized in SQL,
// so every table this package ever touches must come from this fixed map.
var catalogueBrandTables = map[string]string{
	"dabur":   "brand_dabur",
	"manna":   "brand_manna",
	"naga":    "brand_naga",
	"nestle":  "brand_nestle",
	"pepsico": "brand_pepsico",
	"sakthi":  "brand_sakthi",
}

var ErrUnknownBrand = errors.New("unknown brand")

// ErrCatalogueDBUnavailable is returned by every method below when the
// catalogue DB connection wasn't configured (db.CatalogueDB is nil), so
// callers get a normal error instead of a nil-pointer panic.
var ErrCatalogueDBUnavailable = errors.New("catalogue database is not configured")

// catalogueProductColumns casts the text[] columns to text: GORM's raw
// scan-into-struct silently drops slice-kind destination fields, so they
// are read as text here and parsed into []string in scanProductRow.
const catalogueProductColumns = `id, product_name, title, description, category, image_id, size,
	variant_key, product_sku, sku_source, price_range, providers::text AS providers, fssai_license,
	highlights::text AS highlights, nutrients::text AS nutrients, search_query, created_at, updated_at`

// catalogueProductRow mirrors catalogueProductColumns for scanning; array
// columns land here as their raw Postgres text[] literal.
type catalogueProductRow struct {
	ID           int64
	ProductName  string
	Title        string
	Description  string
	Category     string
	ImageID      string
	Size         string
	VariantKey   string
	ProductSKU   string
	SKUSource    string
	PriceRange   string
	Providers    string
	FSSAILicense string
	Highlights   string
	Nutrients    string
	SearchQuery  string
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

func (row catalogueProductRow) toModel(brand string) models.CatalogueProduct {
	return models.CatalogueProduct{
		ID:           row.ID,
		Brand:        brand,
		ProductName:  row.ProductName,
		Title:        row.Title,
		Description:  row.Description,
		Category:     row.Category,
		ImageID:      row.ImageID,
		Images:       db.GetImages(brand, row.ImageID),
		Size:         row.Size,
		VariantKey:   row.VariantKey,
		ProductSKU:   row.ProductSKU,
		SKUSource:    row.SKUSource,
		PriceRange:   row.PriceRange,
		Providers:    models.ParsePGArray(row.Providers),
		FSSAILicense: row.FSSAILicense,
		Highlights:   models.ParsePGArray(row.Highlights),
		Nutrients:    models.ParsePGArray(row.Nutrients),
		SearchQuery:  row.SearchQuery,
		CreatedAt:    row.CreatedAt,
		UpdatedAt:    row.UpdatedAt,
	}
}

type CatalogueRepository interface {
	GetBrands() ([]models.CatalogueBrand, error)
	GetCategories(brand string) ([]string, error)
	GetProducts(brand, category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error)
	GetProductBySKU(brand, sku string) (*models.CatalogueProduct, error)
	GetProductByID(brand string, id int64) (*models.CatalogueProduct, error)
}

type catalogueRepository struct {
	db *gorm.DB
}

// NewCatalogueRepository takes the dedicated catalogue DB connection
// (db.CatalogueDB), never the main nearledb connection.
func NewCatalogueRepository(db *gorm.DB) CatalogueRepository {
	return &catalogueRepository{db: db}
}

func tableForBrand(brand string) (string, error) {
	table, ok := catalogueBrandTables[strings.ToLower(strings.TrimSpace(brand))]
	if !ok {
		return "", ErrUnknownBrand
	}
	return table, nil
}

func (r *catalogueRepository) GetBrands() ([]models.CatalogueBrand, error) {
	if r.db == nil {
		return nil, ErrCatalogueDBUnavailable
	}

	var brands []models.CatalogueBrand

	for brand, table := range catalogueBrandTables {
		var count int64
		if err := r.db.Table(table).Count(&count).Error; err != nil {
			return nil, fmt.Errorf("counting %s: %w", table, err)
		}
		brands = append(brands, models.CatalogueBrand{Brand: brand, ProductCount: count})
	}

	return brands, nil
}

func (r *catalogueRepository) GetCategories(brand string) ([]string, error) {
	if r.db == nil {
		return nil, ErrCatalogueDBUnavailable
	}

	table, err := tableForBrand(brand)
	if err != nil {
		return nil, err
	}

	var categories []string
	query := fmt.Sprintf(`SELECT DISTINCT category FROM %s WHERE category IS NOT NULL ORDER BY category`, table)
	if err := r.db.Raw(query).Scan(&categories).Error; err != nil {
		return nil, err
	}

	return categories, nil
}

// GetProducts lists catalogue products. When brand is empty it browses the
// entire catalogue (every brand merged), which is the entry point for "show
// everything, then let the store owner choose" — brand/category/keyword are
// optional narrowing filters on top of that, not prerequisites.
func (r *catalogueRepository) GetProducts(brand, category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error) {
	if r.db == nil {
		return nil, 0, ErrCatalogueDBUnavailable
	}

	if pagesize <= 0 {
		pagesize = 20
	}
	if pageno <= 0 {
		pageno = 1
	}

	if brand != "" {
		return r.getProductsForBrand(brand, category, keyword, pageno, pagesize)
	}
	return r.getProductsAllBrands(category, keyword, pageno, pagesize)
}

func (r *catalogueRepository) getProductsForBrand(brand, category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error) {
	table, err := tableForBrand(brand)
	if err != nil {
		return nil, 0, err
	}

	offset := (pageno - 1) * pagesize
	whereClause, args := catalogueWhereClause(category, keyword)

	var total int64
	countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s`, table, whereClause)
	if err := r.db.Raw(countQuery, args...).Scan(&total).Error; err != nil {
		return nil, 0, err
	}

	var rows []catalogueProductRow
	dataQuery := fmt.Sprintf(
		`SELECT %s FROM %s WHERE %s ORDER BY id LIMIT ? OFFSET ?`,
		catalogueProductColumns, table, whereClause,
	)
	dataArgs := append(append([]interface{}{}, args...), pagesize, offset)
	if err := r.db.Raw(dataQuery, dataArgs...).Scan(&rows).Error; err != nil {
		return nil, 0, err
	}

	brandKey := strings.ToLower(brand)
	products := make([]models.CatalogueProduct, len(rows))
	for i, row := range rows {
		products[i] = row.toModel(brandKey)
	}

	return products, total, nil
}

// getProductsAllBrands merges matching products across every brand table so
// the catalogue can be browsed without picking a brand first. The whole
// catalogue is currently a few hundred rows total, so fetching each brand's
// filtered set and merging/sorting in Go is simple and fast; if it grows
// much larger this should move to a single UNION ALL query instead.
func (r *catalogueRepository) getProductsAllBrands(category, keyword string, pageno, pagesize int) ([]models.CatalogueProduct, int64, error) {
	whereClause, args := catalogueWhereClause(category, keyword)

	brands := make([]string, 0, len(catalogueBrandTables))
	for brand := range catalogueBrandTables {
		brands = append(brands, brand)
	}
	sort.Strings(brands)

	var all []models.CatalogueProduct
	for _, brand := range brands {
		table := catalogueBrandTables[brand]

		var rows []catalogueProductRow
		dataQuery := fmt.Sprintf(`SELECT %s FROM %s WHERE %s ORDER BY id`, catalogueProductColumns, table, whereClause)
		if err := r.db.Raw(dataQuery, args...).Scan(&rows).Error; err != nil {
			return nil, 0, fmt.Errorf("querying %s: %w", table, err)
		}
		for _, row := range rows {
			all = append(all, row.toModel(brand))
		}
	}

	sort.Slice(all, func(i, j int) bool {
		return strings.ToLower(all[i].ProductName) < strings.ToLower(all[j].ProductName)
	})

	total := int64(len(all))
	offset := (pageno - 1) * pagesize
	if offset > len(all) {
		offset = len(all)
	}
	end := offset + pagesize
	if end > len(all) {
		end = len(all)
	}

	return all[offset:end], total, nil
}

// catalogueWhereClause builds the shared category/keyword filter used by
// both single-brand and all-brand product queries.
func catalogueWhereClause(category, keyword string) (string, []interface{}) {
	where := []string{"1=1"}
	args := []interface{}{}

	if category != "" {
		where = append(where, "category = ?")
		args = append(args, category)
	}
	if keyword != "" {
		where = append(where, "(product_name ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
		like := "%" + keyword + "%"
		args = append(args, like, like, like)
	}

	return strings.Join(where, " AND "), args
}

func (r *catalogueRepository) GetProductBySKU(brand, sku string) (*models.CatalogueProduct, error) {
	if r.db == nil {
		return nil, ErrCatalogueDBUnavailable
	}

	table, err := tableForBrand(brand)
	if err != nil {
		return nil, err
	}

	var row catalogueProductRow
	query := fmt.Sprintf(
		`SELECT %s FROM %s WHERE product_sku = ? LIMIT 1`,
		catalogueProductColumns, table,
	)
	result := r.db.Raw(query, sku).Scan(&row)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, nil
	}

	product := row.toModel(strings.ToLower(brand))
	return &product, nil
}

func (r *catalogueRepository) GetProductByID(brand string, id int64) (*models.CatalogueProduct, error) {
	if r.db == nil {
		return nil, ErrCatalogueDBUnavailable
	}

	table, err := tableForBrand(brand)
	if err != nil {
		return nil, err
	}

	var row catalogueProductRow
	query := fmt.Sprintf(
		`SELECT %s FROM %s WHERE id = ? LIMIT 1`,
		catalogueProductColumns, table,
	)
	result := r.db.Raw(query, id).Scan(&row)
	if result.Error != nil {
		return nil, result.Error
	}
	if result.RowsAffected == 0 {
		return nil, nil
	}

	product := row.toModel(strings.ToLower(brand))
	return &product, nil
}
