package store

// Every query against Drizzle-owned tables (users, events, tickets,
// payments) lives in this file so the read-coupling surface stays small
// and auditable. Column semantics follow backend/src/db/schema.ts.

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

type SessionUser struct {
	ID            string
	Email         string
	Role          string
	AccountStatus string
	Banned        bool
	ExpiresAt     time.Time
}

// GetSessionUser resolves a Better Auth session token (from the
// spanglish.session_token cookie) to its user. Sessions live in the shared
// auth_sessions table, written by the backend; the row's presence is the
// authoritative validity check, so revocations apply here instantly.
func (db *DB) GetSessionUser(ctx context.Context, token string) (SessionUser, error) {
	row := db.QueryRowContext(ctx, db.Rebind(`
		SELECT u.id, u.email, u.role, u.account_status, u.banned, s.expires_at
		FROM auth_sessions s
		JOIN users u ON u.id = s.user_id
		WHERE s.token = ?`), token)
	var id, email, role, status, banned, expires any
	if err := row.Scan(&id, &email, &role, &status, &banned, &expires); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return SessionUser{}, ErrNotFound
		}
		return SessionUser{}, err
	}
	return SessionUser{
		ID:            asString(id),
		Email:         asString(email),
		Role:          asString(role),
		AccountStatus: asString(status),
		Banned:        asBool(banned),
		ExpiresAt:     asTimeOrMillis(expires),
	}, nil
}

// asBool normalizes booleans across drivers (pg BOOLEAN, sqlite INTEGER 0/1).
func asBool(v any) bool {
	switch x := v.(type) {
	case bool:
		return x
	case int64, int, int32, float64:
		return asInt(v) != 0
	case []byte:
		return len(x) > 0 && x[0] != '0' && x[0] != 'f' && x[0] != 'F'
	default:
		return false
	}
}

// asTimeOrMillis parses a timestamp that is a pg TIMESTAMP on postgres but an
// epoch-milliseconds INTEGER on sqlite (Drizzle timestamp_ms mode).
func asTimeOrMillis(v any) time.Time {
	switch v.(type) {
	case int64, int, int32, float64:
		return time.UnixMilli(asInt(v)).UTC()
	default:
		return asTime(v)
	}
}

// HasPaidTicket implements the review decision: a confirmed/checked-in
// ticket whose payment is 'paid', or any confirmed/checked-in ticket when
// the event is free (price = 0).
func (db *DB) HasPaidTicket(ctx context.Context, userID, eventID string) (bool, error) {
	var v any
	err := db.QueryRowContext(ctx, db.Rebind(`
		SELECT 1
		FROM tickets t
		JOIN events e ON e.id = t.event_id
		LEFT JOIN payments p ON p.ticket_id = t.id
		WHERE t.user_id = ? AND t.event_id = ?
		  AND t.status IN ('confirmed','checked_in')
		  AND (p.status = 'paid' OR e.price = 0)
		LIMIT 1`), userID, eventID).Scan(&v)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	return err == nil, err
}

type EventSummary struct {
	ID            string
	Slug          string
	Title         string
	TitleEs       string
	StartDatetime time.Time
	Status        string
}

func (db *DB) GetEventSummary(ctx context.Context, eventID string) (EventSummary, error) {
	return db.getEventWhere(ctx, "id = ?", eventID)
}

// GetEventSummaryBySlug resolves the /events/{slug}/gallery public route.
func (db *DB) GetEventSummaryBySlug(ctx context.Context, slug string) (EventSummary, error) {
	return db.getEventWhere(ctx, "slug = ?", slug)
}

func (db *DB) getEventWhere(ctx context.Context, where string, arg any) (EventSummary, error) {
	row := db.QueryRowContext(ctx, db.Rebind(
		"SELECT id, slug, title, title_es, start_datetime, status FROM events WHERE "+where), arg)
	var id, slug, title, titleEs, start, status any
	if err := row.Scan(&id, &slug, &title, &titleEs, &start, &status); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return EventSummary{}, ErrNotFound
		}
		return EventSummary{}, err
	}
	return EventSummary{
		ID:            asString(id),
		Slug:          asString(slug),
		Title:         asString(title),
		TitleEs:       asString(titleEs),
		StartDatetime: asTime(start),
		Status:        asString(status),
	}, nil
}

// CheckMainSchema fails fast at startup if the columns this service reads
// from Drizzle-owned tables have been renamed or dropped.
func (db *DB) CheckMainSchema(ctx context.Context) error {
	checks := []string{
		"SELECT id, email, role, account_status, banned FROM users WHERE 1 = 0",
		"SELECT id, user_id, token, expires_at FROM auth_sessions WHERE 1 = 0",
		"SELECT id, slug, title, title_es, start_datetime, status, price FROM events WHERE 1 = 0",
		"SELECT id, user_id, event_id, status FROM tickets WHERE 1 = 0",
		"SELECT id, ticket_id, status FROM payments WHERE 1 = 0",
	}
	for _, q := range checks {
		if _, err := db.ExecContext(ctx, q); err != nil {
			return fmt.Errorf("main app schema check failed (%s): %w", q, err)
		}
	}
	return nil
}
