package tools
import (
"context"
"database/sql"
"fmt"
"skraak/db"
"skraak/utils"
"strings"
)
type LocationInput struct {
ID *string `json:"id,omitempty"`
DatasetID *string `json:"dataset_id,omitempty"`
Name *string `json:"name,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
TimezoneID *string `json:"timezone_id,omitempty"`
Description *string `json:"description,omitempty"`
}
type LocationOutput struct {
Location db.Location `json:"location"`
Message string `json:"message"`
}
func CreateOrUpdateLocation(
ctx context.Context,
input LocationInput,
) (LocationOutput, error) {
if input.ID != nil && strings.TrimSpace(*input.ID) != "" {
return updateLocation(ctx, input)
}
return createLocation(ctx, input)
}
func validateLocationFields(input LocationInput) error {
if err := utils.ValidateOptionalStringLength(input.Name, "name", utils.MaxNameLen); err != nil {
return err
}
if err := utils.ValidateOptionalStringLength(input.Description, "description", utils.MaxDescriptionLen); err != nil {
return err
}
if input.Latitude != nil {
if err := utils.ValidateRange(*input.Latitude, "latitude", -90.0, 90.0); err != nil {
return err
}
}
if input.Longitude != nil {
if err := utils.ValidateRange(*input.Longitude, "longitude", -180.0, 180.0); err != nil {
return err
}
}
if input.TimezoneID != nil {
if err := utils.ValidateStringLength(*input.TimezoneID, "timezone_id", utils.MaxTimezoneLen); err != nil {
return err
}
if err := utils.ValidateTimezone(*input.TimezoneID); err != nil {
return err
}
}
return nil
}
func validateCreateFields(input LocationInput) error {
if input.DatasetID == nil || strings.TrimSpace(*input.DatasetID) == "" {
return fmt.Errorf("dataset_id is required when creating a location")
}
if input.Name == nil || strings.TrimSpace(*input.Name) == "" {
return fmt.Errorf("name is required when creating a location")
}
if input.Latitude == nil {
return fmt.Errorf("latitude is required when creating a location")
}
if input.Longitude == nil {
return fmt.Errorf("longitude is required when creating a location")
}
if input.TimezoneID == nil || strings.TrimSpace(*input.TimezoneID) == "" {
return fmt.Errorf("timezone_id is required when creating a location")
}
if err := utils.ValidateShortID(*input.DatasetID, "dataset_id"); err != nil {
return err
}
return validateLocationFields(input)
}
func fetchLocationByID(ctx context.Context, queryer interface {
QueryRowContext(context.Context, string, ...any) *sql.Row
}, id string) (db.Location, error) {
const selectCols = "SELECT id, dataset_id, name, latitude, longitude, description, created_at, last_modified, active, timezone_id FROM location WHERE id = ?"
var loc db.Location
err := queryer.QueryRowContext(ctx, selectCols, id).Scan(
&loc.ID, &loc.DatasetID, &loc.Name, &loc.Latitude, &loc.Longitude,
&loc.Description, &loc.CreatedAt, &loc.LastModified, &loc.Active, &loc.TimezoneID)
return loc, err
}
func createLocation(ctx context.Context, input LocationInput) (LocationOutput, error) {
var output LocationOutput
if err := validateCreateFields(input); err != nil {
return output, err
}
err := db.WithWriteTx(ctx, dbPath, "create_or_update_location", func(database *sql.DB, tx *db.LoggedTx) error {
if err := verifyDatasetExistsAndActive(ctx, tx, *input.DatasetID); err != nil {
return err
}
var existingID string
qerr := tx.QueryRowContext(ctx,
"SELECT id FROM location WHERE dataset_id = ? AND name = ? AND active = true",
*input.DatasetID, *input.Name,
).Scan(&existingID)
if qerr == nil {
result, rerr := returnExistingLocation(ctx, tx, existingID)
if rerr != nil {
return rerr
}
output = result
return nil
}
id, gerr := utils.GenerateShortID()
if gerr != nil {
return fmt.Errorf("failed to generate ID: %w", gerr)
}
if _, err := tx.ExecContext(ctx,
"INSERT INTO location (id, dataset_id, name, latitude, longitude, timezone_id, description, created_at, last_modified, active) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, TRUE)",
id, *input.DatasetID, *input.Name, *input.Latitude, *input.Longitude, *input.TimezoneID, input.Description,
); err != nil {
return fmt.Errorf("failed to create location: %w", err)
}
location, ferr := fetchLocationByID(ctx, tx, id)
if ferr != nil {
return fmt.Errorf("failed to fetch created location: %w", ferr)
}
output.Location = location
output.Message = fmt.Sprintf("Successfully created location '%s' with ID %s (%.6f, %.6f, %s)",
location.Name, location.ID, location.Latitude, location.Longitude, location.TimezoneID)
return nil
})
return output, err
}
func returnExistingLocation(ctx context.Context, tx *db.LoggedTx, existingID string) (LocationOutput, error) {
var output LocationOutput
location, err := fetchLocationByID(ctx, tx, existingID)
if err != nil {
return output, fmt.Errorf("failed to fetch existing location: %w", err)
}
output.Location = location
output.Message = fmt.Sprintf("Location '%s' already exists in dataset (ID: %s) - returning existing location", location.Name, location.ID)
return output, nil
}
func verifyDatasetExistsAndActive(ctx context.Context, q db.Querier, datasetID string) error {
_, err := db.DatasetExistsAndActive(q, datasetID)
return err
}
func updateLocation(ctx context.Context, input LocationInput) (LocationOutput, error) {
var output LocationOutput
locationID := *input.ID
if err := utils.ValidateShortID(locationID, "location_id"); err != nil {
return output, err
}
if err := validateLocationFields(input); err != nil {
return output, err
}
if err := utils.ValidateOptionalShortID(input.DatasetID, "dataset_id"); err != nil {
return output, err
}
err := db.WithWriteTx(ctx, dbPath, "create_or_update_location", func(database *sql.DB, tx *db.LoggedTx) error {
if err := verifyLocationExistsAndActive(database, locationID); err != nil {
return err
}
if input.DatasetID != nil {
if err := verifyDatasetExistsAndActive(ctx, database, *input.DatasetID); err != nil {
return err
}
}
updates, args, uerr := buildLocationUpdates(input, locationID)
if uerr != nil {
return uerr
}
query := fmt.Sprintf("UPDATE location SET %s WHERE id = ?", strings.Join(updates, ", "))
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return fmt.Errorf("failed to update location: %w", err)
}
location, ferr := fetchLocationByID(ctx, tx, locationID)
if ferr != nil {
return fmt.Errorf("failed to fetch updated location: %w", ferr)
}
output.Location = location
output.Message = fmt.Sprintf("Successfully updated location '%s' (ID: %s)", location.Name, location.ID)
return nil
})
return output, err
}
func verifyLocationExistsAndActive(queryer interface {
QueryRow(string, ...any) *sql.Row
}, locationID string) error {
var exists, active bool
err := queryer.QueryRow(
"SELECT EXISTS(SELECT 1 FROM location WHERE id = ?), COALESCE((SELECT active FROM location WHERE id = ?), false)",
locationID, locationID,
).Scan(&exists, &active)
if err != nil {
return fmt.Errorf("failed to query location: %w", err)
}
if !exists {
return fmt.Errorf("location not found: %s", locationID)
}
if !active {
return fmt.Errorf("location '%s' is not active (cannot update inactive locations)", locationID)
}
return nil
}
func buildLocationUpdates(input LocationInput, locationID string) ([]string, []any, error) {
updates := []string{}
args := []any{}
if input.DatasetID != nil {
updates = append(updates, "dataset_id = ?")
args = append(args, *input.DatasetID)
}
if input.Name != nil {
updates = append(updates, "name = ?")
args = append(args, *input.Name)
}
if input.Latitude != nil {
updates = append(updates, "latitude = ?")
args = append(args, *input.Latitude)
}
if input.Longitude != nil {
updates = append(updates, "longitude = ?")
args = append(args, *input.Longitude)
}
if input.Description != nil {
updates = append(updates, "description = ?")
args = append(args, *input.Description)
}
if input.TimezoneID != nil {
updates = append(updates, "timezone_id = ?")
args = append(args, *input.TimezoneID)
}
if len(updates) == 0 {
return nil, nil, fmt.Errorf("no fields provided to update")
}
updates = append(updates, "last_modified = now()")
args = append(args, locationID)
return updates, args, nil
}