package store

import (
	"context"
	"database/sql"
	"errors"
	"time"
)

var ErrNotFound = errors.New("not found")

const (
	VisibilityPublic  = "public"
	VisibilityPrivate = "private"
	VisibilityLink    = "link"
	VisibilityTicket  = "ticket"
)

type Gallery struct {
	ID            string
	Slug          string
	Title         string
	TitleEs       string
	Description   string
	DescriptionEs string
	EventID       string
	Visibility    string
	ShareToken    string
	CoverPhotoID  string
	CreatedBy     string
	CreatedAt     time.Time
	UpdatedAt     time.Time
	PhotoCount    int
}

const galleryColumns = `g.id, g.slug, g.title, g.title_es, g.description, g.description_es,
	g.event_id, g.visibility, g.share_token, g.cover_photo_id, g.created_by, g.created_at, g.updated_at`

type scanner interface{ Scan(...any) error }

func scanGallery(s scanner, withCount bool) (Gallery, error) {
	var v [13]any
	dest := make([]any, 0, 14)
	for i := range v {
		dest = append(dest, &v[i])
	}
	var count any
	if withCount {
		dest = append(dest, &count)
	}
	if err := s.Scan(dest...); err != nil {
		return Gallery{}, err
	}
	g := Gallery{
		ID:            asString(v[0]),
		Slug:          asString(v[1]),
		Title:         asString(v[2]),
		TitleEs:       asString(v[3]),
		Description:   asString(v[4]),
		DescriptionEs: asString(v[5]),
		EventID:       asString(v[6]),
		Visibility:    asString(v[7]),
		ShareToken:    asString(v[8]),
		CoverPhotoID:  asString(v[9]),
		CreatedBy:     asString(v[10]),
		CreatedAt:     asTime(v[11]),
		UpdatedAt:     asTime(v[12]),
	}
	if withCount {
		g.PhotoCount = int(asInt(count))
	}
	return g, nil
}

func (db *DB) CreateGallery(ctx context.Context, g Gallery) error {
	_, err := db.ExecContext(ctx, db.Rebind(`
		INSERT INTO photos_galleries
			(id, slug, title, title_es, description, description_es, event_id, visibility, share_token, created_by, created_at, updated_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`),
		g.ID, g.Slug, g.Title, nullable(g.TitleEs), nullable(g.Description), nullable(g.DescriptionEs),
		nullable(g.EventID), g.Visibility, g.ShareToken, nullable(g.CreatedBy),
		db.TimeArg(g.CreatedAt), db.TimeArg(g.UpdatedAt))
	return err
}

func (db *DB) GetGallery(ctx context.Context, id string) (Gallery, error) {
	return db.getGalleryWhere(ctx, "g.id = ?", id)
}

func (db *DB) GetGalleryBySlug(ctx context.Context, slug string) (Gallery, error) {
	return db.getGalleryWhere(ctx, "g.slug = ?", slug)
}

// GetGalleryByEventID returns the newest gallery linked to an event, for
// the /events/{slug}/gallery public route.
func (db *DB) GetGalleryByEventID(ctx context.Context, eventID string) (Gallery, error) {
	galleries, err := db.queryGalleries(ctx, `
		SELECT `+galleryColumns+`,
			(SELECT COUNT(*) FROM photos_photos p WHERE p.gallery_id = g.id) AS photo_count
		FROM photos_galleries g
		WHERE g.event_id = ?
		ORDER BY g.created_at DESC LIMIT 1`, eventID)
	if err != nil {
		return Gallery{}, err
	}
	if len(galleries) == 0 {
		return Gallery{}, ErrNotFound
	}
	return galleries[0], nil
}

func (db *DB) getGalleryWhere(ctx context.Context, where string, arg any) (Gallery, error) {
	row := db.QueryRowContext(ctx, db.Rebind(`
		SELECT `+galleryColumns+`,
			(SELECT COUNT(*) FROM photos_photos p WHERE p.gallery_id = g.id) AS photo_count
		FROM photos_galleries g WHERE `+where), arg)
	g, err := scanGallery(row, true)
	if errors.Is(err, sql.ErrNoRows) {
		return Gallery{}, ErrNotFound
	}
	return g, err
}

// ListGalleries returns every gallery (admin view). eventID filters when set.
func (db *DB) ListGalleries(ctx context.Context, eventID string, limit, offset int) ([]Gallery, error) {
	q := `
		SELECT ` + galleryColumns + `,
			(SELECT COUNT(*) FROM photos_photos p WHERE p.gallery_id = g.id) AS photo_count
		FROM photos_galleries g`
	args := []any{}
	if eventID != "" {
		q += " WHERE g.event_id = ?"
		args = append(args, eventID)
	}
	q += " ORDER BY g.created_at DESC LIMIT ? OFFSET ?"
	args = append(args, limit, offset)
	return db.queryGalleries(ctx, q, args...)
}

// ListPublicGalleries returns visibility='public' galleries with at least
// their ready photo counts, newest first.
func (db *DB) ListPublicGalleries(ctx context.Context) ([]Gallery, error) {
	return db.queryGalleries(ctx, `
		SELECT `+galleryColumns+`,
			(SELECT COUNT(*) FROM photos_photos p WHERE p.gallery_id = g.id AND p.status = 'ready') AS photo_count
		FROM photos_galleries g
		WHERE g.visibility = 'public'
		ORDER BY g.created_at DESC`)
}

func (db *DB) queryGalleries(ctx context.Context, q string, args ...any) ([]Gallery, error) {
	rows, err := db.QueryContext(ctx, db.Rebind(q), args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	galleries := []Gallery{}
	for rows.Next() {
		g, err := scanGallery(rows, true)
		if err != nil {
			return nil, err
		}
		galleries = append(galleries, g)
	}
	return galleries, rows.Err()
}

type GalleryUpdate struct {
	Title         *string
	TitleEs       *string
	Description   *string
	DescriptionEs *string
	EventID       *string // empty string clears the link
	Visibility    *string
	CoverPhotoID  *string
}

func (db *DB) UpdateGallery(ctx context.Context, id string, u GalleryUpdate) error {
	set := ""
	args := []any{}
	add := func(col string, val any) {
		if set != "" {
			set += ", "
		}
		set += col + " = ?"
		args = append(args, val)
	}
	if u.Title != nil {
		add("title", *u.Title)
	}
	if u.TitleEs != nil {
		add("title_es", nullable(*u.TitleEs))
	}
	if u.Description != nil {
		add("description", nullable(*u.Description))
	}
	if u.DescriptionEs != nil {
		add("description_es", nullable(*u.DescriptionEs))
	}
	if u.EventID != nil {
		add("event_id", nullable(*u.EventID))
	}
	if u.Visibility != nil {
		add("visibility", *u.Visibility)
	}
	if u.CoverPhotoID != nil {
		add("cover_photo_id", nullable(*u.CoverPhotoID))
	}
	if set == "" {
		return nil
	}
	add("updated_at", db.TimeArg(time.Now()))
	args = append(args, id)
	res, err := db.ExecContext(ctx, db.Rebind("UPDATE photos_galleries SET "+set+" WHERE id = ?"), args...)
	if err != nil {
		return err
	}
	return errIfNoRows(res)
}

func (db *DB) RotateShareToken(ctx context.Context, id, token string) error {
	res, err := db.ExecContext(ctx,
		db.Rebind("UPDATE photos_galleries SET share_token = ?, updated_at = ? WHERE id = ?"),
		token, db.TimeArg(time.Now()), id)
	if err != nil {
		return err
	}
	return errIfNoRows(res)
}

func (db *DB) DeleteGallery(ctx context.Context, id string) error {
	res, err := db.ExecContext(ctx, db.Rebind("DELETE FROM photos_galleries WHERE id = ?"), id)
	if err != nil {
		return err
	}
	return errIfNoRows(res)
}

func (db *DB) SlugExists(ctx context.Context, slug string) (bool, error) {
	var v any
	err := db.QueryRowContext(ctx, db.Rebind("SELECT 1 FROM photos_galleries WHERE slug = ?"), slug).Scan(&v)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	return err == nil, err
}

func errIfNoRows(res sql.Result) error {
	n, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrNotFound
	}
	return nil
}

// nullable maps "" to NULL so optional text columns stay NULL not ”.
func nullable(s string) any {
	if s == "" {
		return nil
	}
	return s
}

func asInt(v any) int64 {
	switch x := v.(type) {
	case int64:
		return x
	case int:
		return int64(x)
	case int32:
		return int64(x)
	case float64:
		return int64(x)
	case []byte:
		var n int64
		for _, c := range x {
			if c < '0' || c > '9' {
				break
			}
			n = n*10 + int64(c-'0')
		}
		return n
	default:
		return 0
	}
}
