package repositories

import (
	"astrology-api/constants"
	models "astrology-api/models/usermodel"
	"errors"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

// The reads and writes behind the request -> accept -> start handshake, the
// availability gate, the server-authoritative timer and the chat transcript.
//
// Split out of consultation_repository.go, which is already the billing
// layer's own file and long enough.

//////////////////////////////////////////////////////////////
// Availability
//////////////////////////////////////////////////////////////

func (r *consultationRepository) GetAstrologerAvailability(
	astrologerID uint,
) (*AstrologerAvailability, error) {

	var availability AstrologerAvailability

	err := r.db.
		Table("astrologer_status").
		Select("is_online, is_chat_online, is_call_online, is_busy, quick_dnd").
		Where("astrologer_id = ?", astrologerID).
		Where("is_delete = ?", false).
		Take(&availability).Error

	if err != nil {

		// No row is not an error. astrologer_status is empty on this
		// database, so the service decides what that means via
		// ConsultationAvailabilityStrict rather than every caller guessing.
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return &AstrologerAvailability{Found: false}, nil
		}

		return nil, err
	}

	availability.Found = true

	return &availability, nil
}

func (r *consultationRepository) SetAstrologerBusy(
	tx *gorm.DB,
	astrologerID uint,
	busy bool,
) error {

	db := tx

	if db == nil {
		db = r.db
	}

	now := time.Now()

	updates := map[string]interface{}{
		"is_busy":        busy,
		"last_active_at": now,
		"updated_at":     now,
	}

	if busy {

		updates["busy_at"] = now
		updates["current_status"] = "busy"

	} else {

		updates["busy_at"] = nil

		// The enum has no "available" member, so coming off busy means
		// falling back to whatever the online flag says. Copied from the
		// astrologer stack's own status writer so the two agree.
		updates["current_status"] = gorm.Expr("IF(is_online, 'online', 'offline')")
	}

	// A missing row updates nothing, which is not an error: the gate already
	// handles an astrologer with no status row.
	if err := db.
		Table("astrologer_status").
		Where("astrologer_id = ?", astrologerID).
		Updates(updates).Error; err != nil {
		return err
	}

	// Mirror it onto the astrologers row: is_available 2 means busy in a
	// session. Coming off busy only touches a row still marked BUSY, so an
	// astrologer who went offline or logged out mid-session stays OFFLINE.
	if busy {
		return db.
			Table("astrologers").
			Where("id = ?", astrologerID).
			Updates(map[string]interface{}{"status": "BUSY", "is_available": 2}).Error
	}

	return db.
		Table("astrologers").
		Where("id = ? AND status = ?", astrologerID, "BUSY").
		Updates(map[string]interface{}{"status": "ONLINE", "is_available": 1}).Error
}

func (r *consultationRepository) ReconcileAstrologerBusy() (int64, error) {

	result := r.db.
		Table("astrologer_status").
		Where("is_busy = ?", true).
		Where(`astrologer_id NOT IN (
			SELECT astrologerId FROM consultations
			 WHERE status IN (?) AND isDelete = ?
		)`, []string{
			constants.ConsultationStatusRequested,
			constants.ConsultationStatusAccepted,
			constants.ConsultationStatusOngoing,
		}, false).
		Updates(map[string]interface{}{
			"is_busy":        false,
			"busy_at":        nil,
			"current_status": gorm.Expr("IF(is_online, 'online', 'offline')"),
			"updated_at":     time.Now(),
		})

	return result.RowsAffected, result.Error
}

func (r *consultationRepository) CountOpenForAstrologer(
	astrologerID uint,
) (int64, error) {

	var total int64

	err := r.db.
		Model(&models.Consultation{}).
		Where("astrologerId = ?", astrologerID).
		Where("status IN ?", openStatuses()).
		Where("isDelete = ?", false).
		Count(&total).Error

	return total, err
}

func (r *consultationRepository) LockOpenForAstrologer(
	tx *gorm.DB,
	astrologerID uint,
) ([]models.Consultation, error) {

	db := tx

	if db == nil {
		db = r.db
	}

	var records []models.Consultation

	err := db.
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("astrologerId = ?", astrologerID).
		Where("status IN ?", openStatuses()).
		Where("isDelete = ?", false).
		Order("id ASC").
		Find(&records).Error

	return records, err
}

//////////////////////////////////////////////////////////////
// The handshake
//////////////////////////////////////////////////////////////

func (r *consultationRepository) GetOpenConsultation(
	userID uint,
) (*models.Consultation, error) {

	var consultation models.Consultation

	err := r.db.
		Where("userId = ?", userID).
		Where("status IN ?", openStatuses()).
		Where("isDelete = ?", false).
		Order("id DESC").
		First(&consultation).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &consultation, nil
}

func (r *consultationRepository) ListAstrologerRequests(
	astrologerID uint,
	limit int,
) ([]models.Consultation, error) {

	if limit <= 0 {
		limit = 20
	}

	var records []models.Consultation

	err := r.db.
		Where("astrologerId = ?", astrologerID).
		Where("status = ?", constants.ConsultationStatusRequested).
		Where("isDelete = ?", false).
		Order("requestedAt ASC, id ASC").
		Limit(limit).
		Find(&records).Error

	return records, err
}

func (r *consultationRepository) GetExpiredRequested(
	before time.Time,
	limit int,
) ([]models.Consultation, error) {

	if limit <= 0 {
		limit = 100
	}

	var records []models.Consultation

	err := r.db.
		Where("status = ?", constants.ConsultationStatusRequested).
		Where("isDelete = ?", false).
		Where("COALESCE(requestedAt, created_at) < ?", before).
		Order("id ASC").
		Limit(limit).
		Find(&records).Error

	return records, err
}

func (r *consultationRepository) GetUnjoinedAccepted(
	before time.Time,
	limit int,
) ([]models.Consultation, error) {

	if limit <= 0 {
		limit = 100
	}

	var records []models.Consultation

	// startedAt IS NULL is what "the customer never opened it" means: accept
	// sets acceptedAt, and only the customer's start sets startedAt.
	err := r.db.
		Where("status = ?", constants.ConsultationStatusAccepted).
		Where("isDelete = ?", false).
		Where("startedAt IS NULL").
		Where("COALESCE(acceptedAt, created_at) < ?", before).
		Order("id ASC").
		Limit(limit).
		Find(&records).Error

	return records, err
}

func (r *consultationRepository) SetChannelName(
	id uint,
	channelName string,
) error {

	return r.db.
		Model(&models.Consultation{}).
		Where("id = ?", id).
		Updates(map[string]interface{}{
			"channelName": channelName,
			"updated_at":  time.Now(),
		}).Error
}

//////////////////////////////////////////////////////////////
// The server-authoritative timer
//////////////////////////////////////////////////////////////

func (r *consultationRepository) GetOverrunConsultations(
	now time.Time,
	limit int,
) ([]models.Consultation, error) {

	if limit <= 0 {
		limit = 100
	}

	var records []models.Consultation

	err := r.db.
		Where("status IN ?", liveStatuses()).
		Where("isDelete = ?", false).
		Where("startedAt IS NOT NULL").
		Where("maxBillableSeconds > 0").
		Where("DATE_ADD(startedAt, INTERVAL maxBillableSeconds SECOND) <= ?", now).
		Order("id ASC").
		Limit(limit).
		Find(&records).Error

	return records, err
}

func (r *consultationRepository) GetPendingWarnings(
	now time.Time,
	warnSeconds int,
	limit int,
) ([]models.Consultation, error) {

	if limit <= 0 {
		limit = 100
	}

	var records []models.Consultation

	err := r.db.
		Where("status IN ?", liveStatuses()).
		Where("isDelete = ?", false).
		Where("warnedAt IS NULL").
		Where("startedAt IS NOT NULL").
		Where("maxBillableSeconds > ?", warnSeconds).
		Where(
			"DATE_ADD(startedAt, INTERVAL (maxBillableSeconds - ?) SECOND) <= ?",
			warnSeconds,
			now,
		).
		Order("id ASC").
		Limit(limit).
		Find(&records).Error

	return records, err
}

func (r *consultationRepository) ClaimWarning(
	id uint,
	at time.Time,
) (bool, error) {

	// The WHERE clause is the guard. Whoever updates the row is the one that
	// sends the warning; everyone else updates nothing and stays quiet.
	result := r.db.
		Model(&models.Consultation{}).
		Where("id = ?", id).
		Where("warnedAt IS NULL").
		Updates(map[string]interface{}{
			"warnedAt":   at,
			"updated_at": at,
		})

	if result.Error != nil {
		return false, result.Error
	}

	return result.RowsAffected == 1, nil
}

//////////////////////////////////////////////////////////////
// Free chat
//////////////////////////////////////////////////////////////

func (r *consultationRepository) HasUsedFreeChat(
	userID uint,
	minConnectedSeconds int,
) (bool, error) {
	var total int64
	err := r.db.
		Model(&models.Consultation{}).
		Where("userId = ?", userID).
		Where("freeMinutes > ?", 0).
		Where("isDelete = ?", false).
		Where(
			r.db.
				Where("status IN ?", liveStatuses()).
				Or(
					r.db.
						Where("status = ?", constants.ConsultationStatusCompleted).
						Where("durationSeconds >= ?", minConnectedSeconds),
				),
		).
		Limit(1).
		Count(&total).Error

	if err != nil {
		return false, err
	}

	return total > 0, nil
}

//////////////////////////////////////////////////////////////
// Transcript
//////////////////////////////////////////////////////////////

func (r *consultationRepository) ExistingClientMessageIDs(
	consultationID uint,
	ids []string,
) (map[string]bool, error) {

	existing := make(map[string]bool, len(ids))

	if len(ids) == 0 {
		return existing, nil
	}

	var found []string

	err := r.db.
		Model(&models.ChatMessage{}).
		Where("consultationId = ?", consultationID).
		Where("clientMessageId IN ?", ids).
		Pluck("clientMessageId", &found).Error

	if err != nil {
		return nil, err
	}

	for _, id := range found {
		existing[id] = true
	}

	return existing, nil
}

func (r *consultationRepository) CreateChatMessages(
	tx *gorm.DB,
	messages []models.ChatMessage,
) error {

	if len(messages) == 0 {
		return nil
	}

	db := tx

	if db == nil {
		db = r.db
	}

	// DoNothing rather than an update: the first upload defines the record.
	// Letting the second uploader overwrite would mean one party could
	// rewrite the other's words in the transcript support reads to settle a
	// dispute.
	//
	// This is the race guard between two simultaneous uploads; the counts
	// reported back to the app come from ExistingClientMessageIDs, because
	// RowsAffected is not trustworthy under an upsert.
	return db.
		Clauses(clause.OnConflict{
			Columns: []clause.Column{
				{Name: "consultationId"},
				{Name: "clientMessageId"},
			},
			DoNothing: true,
		}).
		CreateInBatches(messages, 200).Error
}

func (r *consultationRepository) CountChatMessages(
	consultationID uint,
) (int64, error) {

	var total int64

	err := r.db.
		Model(&models.ChatMessage{}).
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Count(&total).Error

	return total, err
}

func (r *consultationRepository) ListChatMessages(
	consultationID uint,
	page int,
	limit int,
) ([]models.ChatMessage, int64, error) {

	if page <= 0 {
		page = 1
	}

	if limit <= 0 {
		limit = 200
	}

	var total int64

	err := r.db.
		Model(&models.ChatMessage{}).
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Count(&total).Error

	if err != nil {
		return nil, 0, err
	}

	var records []models.ChatMessage

	// Ordering is (sentAt, sequence, id): the two devices upload
	// independently, so ids interleave by upload order rather than by time.
	err = r.db.
		Where("consultationId = ?", consultationID).
		Where("isDelete = ?", false).
		Order("sentAt ASC, sequence ASC, id ASC").
		Offset((page - 1) * limit).
		Limit(limit).
		Find(&records).Error

	return records, total, err
}

//////////////////////////////////////////////////////////////
// Misc
//////////////////////////////////////////////////////////////

func (r *consultationRepository) GetUserByID(
	userID uint,
) (*models.User, error) {

	var user models.User

	err := r.db.
		Where("id = ?", userID).
		First(&user).Error

	if err != nil {

		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, nil
		}

		return nil, err
	}

	return &user, nil
}

//////////////////////////////////////////////////////////////
// Status sets
//////////////////////////////////////////////////////////////

// openStatuses is everything that occupies a customer or an astrologer.
func openStatuses() []string {

	return []string{
		constants.ConsultationStatusRequested,
		constants.ConsultationStatusAccepted,
		constants.ConsultationStatusOngoing,
	}
}

// liveStatuses is everything bill() will close.
func liveStatuses() []string {

	return []string{
		constants.ConsultationStatusAccepted,
		constants.ConsultationStatusOngoing,
	}
}
