/*
 * Amsterdam Web Communities System
 * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
// The database package contains database management and storage logic.
package database

import (
	"context"
	"database/sql"
	"errors"
	"slices"
	"strings"
	"sync"

	"git.erbosoft.com/amy/amsterdam/util"
	log "github.com/sirupsen/logrus"
)

// Category is the structure defining a category.
type Category struct {
	CatId         int32  `db:"catid"`
	Parent        int32  `db:"parent"`
	SymLink       int32  `db:"symlink"`
	HideDirectory bool   `db:"hide_dir"`
	HideSearch    bool   `db:"hide_search"`
	Name          string `db:"name"`
}

// Selectors for operator in category search.
const (
	SearchCatOperPrefix    = 0
	SearchCatOperSubstring = 1
	SearchCatOperRegex     = 2
)

// allCategories is the list of all categories loaded from the database.
var allCategories []Category

// categoryIdMap maps IDs to categories.
var categoryIdMap map[int32]*Category = make(map[int32]*Category)

// categoryMutex syncs the loading of the categories.
var categoryMutex sync.Mutex

// isCatEnabled determines if category features are enabled.
func isCatEnabled(ctx context.Context) (bool, error) {
	g, err := AmGlobals(ctx)
	if err != nil {
		return false, err
	}
	set, err := g.Flags(ctx)
	if err != nil {
		return false, err
	}
	return !set.Get(GlobalFlagNoCategories), nil
}

// loadCategories loads the categories list from the database.
func loadCategories(ctx context.Context) error {
	categoryMutex.Lock()
	defer categoryMutex.Unlock()
	if allCategories == nil {
		row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM refcategory")
		var ncats int32
		if err := row.Scan(&ncats); err != nil {
			return err
		}
		allCategories = make([]Category, 0, ncats)
		if err := amdb.SelectContext(ctx, &allCategories, "SELECT * FROM refcategory ORDER BY parent, name"); err != nil {
			return err
		}
		for i, c := range allCategories {
			categoryIdMap[c.CatId] = &(allCategories[i])
		}
	}
	return nil
}

/* AmGetCategory returns the category for the given name.
 * Parameters:
 *     ctx - Standard Go context value.
 *     catid - The ID of the category to get.
 * Returns:
 *     Pointer to the appropriate Category, or nil.
 *     Standard Go error status.
 */
func AmGetCategory(ctx context.Context, catid int32) (*Category, error) {
	ok, err := isCatEnabled(ctx)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, errors.New("category feature not supported")
	}
	if err = loadCategories(ctx); err != nil {
		return nil, err
	}
	c := categoryIdMap[catid]
	d := 5
	for c.SymLink != -1 {
		d--
		if d == 0 {
			return nil, errors.New("symlink resolution error")
		}
		c = categoryIdMap[c.SymLink]
	}
	return c, nil
}

/* AmGetCategoryHierarchy returns the category hierarchy for the given ID.
 * Parameters:
 *     ctx - Standard Go context value.
 *     catid - The ID of the category to get.
 * Returns:
 *     Array of pointers to the categories in hierarchical order, or nil.
 *     Standard Go error status.
 */
func AmGetCategoryHierarchy(ctx context.Context, catid int32) ([]*Category, error) {
	ok, err := isCatEnabled(ctx)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, errors.New("category feature not supported")
	}
	if err = loadCategories(ctx); err != nil {
		return nil, err
	}
	// walk all the way to the "root" (parent = -1)
	p := catid
	ia := make([]*Category, 0, 3)
	for p != -1 {
		c := categoryIdMap[p]
		for c.SymLink != -1 {
			c = categoryIdMap[c.SymLink]
		}
		ia = append(ia, c)
		p = c.Parent
	}
	// reverse the array for return
	rc := make([]*Category, 0, len(ia))
	for i := range ia {
		rc = append(rc, ia[len(ia)-(i+1)])
	}
	return rc, nil
}

/* AmGetSubCategories returns a list of all subcategories of the given category ID.
 * Parameters:
 *     ctx - Standard Go context value.
 *     catid - The parent category ID to use.  May be -1 to return all "top level" categories.
 * Returns:
 *     List of subcategories of this category.
 *     Standard Go error status.
 */
func AmGetSubCategories(ctx context.Context, catid int32) ([]*Category, error) {
	ok, err := isCatEnabled(ctx)
	if err != nil {
		return nil, err
	}
	if !ok {
		return nil, errors.New("category feature not supported")
	}
	if err = loadCategories(ctx); err != nil {
		return nil, err
	}
	rc := make([]*Category, 0)
	for i, cat := range allCategories {
		if catid == cat.Parent {
			rc = append(rc, &(allCategories[i]))
		}
	}
	slices.SortFunc(rc, func(a, b *Category) int {
		return strings.Compare(a.Name, b.Name)
	})
	return rc, nil
}

/* AmSearchCategories searches for categories matching certain criteria.
 * Parameters:
 *     ctx - Standard Go context value.
 *     oper - The operation to perform on the category name:
 *         SearchCatOperPrefix - The category name has the string "term" as a prefix.
 *         SearchCatOperSubstring - The category name contains the string "term".
 *         SearchCatOperRegex - The category name matches the regular expression in "term".
 *     term - The search term, as specified above.
 *     offset - Number of categories to skip at beginning of list.
 *     max - Maximum number of categories to return.
 * Returns:
 *     Array of Category pointers representing the return elements.
 *     The total number of categories matching this query (could be greater than max)
 *	   Standard Go error status.
 */
func AmSearchCategories(ctx context.Context, oper int, term string, offset int, max int, showAll bool, searchAll bool) ([]*Category, int, error) {
	ok, err := isCatEnabled(ctx)
	if err != nil {
		return nil, -1, err
	}
	if !ok {
		return nil, -1, errors.New("category feature not supported")
	}
	var queryString strings.Builder
	queryString.WriteString("name ")
	switch oper {
	case SearchCatOperPrefix:
		queryString.WriteString("LIKE '")
		queryString.WriteString(util.SqlEscape(term, true))
		queryString.WriteString("%'")
	case SearchCatOperSubstring:
		queryString.WriteString("LIKE '%")
		queryString.WriteString(util.SqlEscape(term, true))
		queryString.WriteString("%'")
	case SearchCatOperRegex:
		queryString.WriteString("REGEXP '")
		queryString.WriteString(util.SqlEscape(term, false))
		queryString.WriteString("'")
	default:
		return nil, -1, errors.New("invalid operator to search function")
	}
	if !showAll {
		queryString.WriteString(" AND hide_dir = 0")
	}
	if !searchAll {
		queryString.WriteString(" AND hide_search = 0")
	}
	q := queryString.String()
	row := amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM refcategory WHERE "+q)
	var total int
	if err = row.Scan(&total); err != nil {
		return nil, total, err
	}
	if total == 0 {
		return make([]*Category, 0), 0, nil
	}
	var rs *sql.Rows
	if offset > 0 {
		rs, err = amdb.QueryContext(ctx, "SELECT catid FROM refcategory WHERE "+q+" ORDER BY parent, name LIMIT ? OFFSET ?", max, offset)
	} else {
		rs, err = amdb.QueryContext(ctx, "SELECT catid FROM refcategory WHERE "+q+" ORDER BY parent, name LIMIT ?", max)
	}
	if err != nil {
		return nil, total, err
	}
	rc := make([]*Category, 0, min(max, 1000))
	for rs.Next() {
		var catid int32
		if err = rs.Scan(&catid); err == nil {
			c, err := AmGetCategory(ctx, catid)
			if err == nil {
				rc = append(rc, c)
			}
		}
		if err != nil {
			log.Errorf("AmSearchCategoris scan error: %v", err)
		}
	}
	return rc, total, nil
}
