package service

import (
	"encoding/json"
	"errors"
	"strings"

	"github.com/mhsanaei/3x-ui/v3/internal/database"
	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
	"github.com/mhsanaei/3x-ui/v3/internal/xray"

	"gorm.io/gorm"
)

func (s *ClientService) GetRecordByEmail(tx *gorm.DB, email string) (*model.ClientRecord, error) {
	if tx == nil {
		tx = database.GetDB()
	}
	row := &model.ClientRecord{}
	err := tx.Where("email = ?", email).First(row).Error
	if err != nil {
		return nil, err
	}
	return row, nil
}

// EffectiveFlow returns the client's flow from the first flow-capable inbound
// it is attached to (lowest inbound_id with a non-empty flow_override). The
// canonical clients.Flow column is unreliable for multi-inbound clients: a
// non-flow inbound (Hysteria, WS, gRPC, …) carries an empty flow and, when its
// SyncInbound runs last, overwrites the column to "" even though a VLESS Reality
// inbound stored a real flow. The per-inbound flow_override is always correct,
// so derive the display flow from it (order-independent). See issue #4792.
func (s *ClientService) EffectiveFlow(tx *gorm.DB, recordId int) (string, error) {
	if tx == nil {
		tx = database.GetDB()
	}
	var flows []string
	err := tx.Model(&model.ClientInbound{}).
		Where("client_id = ? AND flow_override <> ?", recordId, "").
		Order("inbound_id ASC").
		Limit(1).
		Pluck("flow_override", &flows).Error
	if err != nil {
		return "", err
	}
	if len(flows) == 0 {
		return "", nil
	}
	return flows[0], nil
}

// EffectiveFlowsByEmails resolves the intended flow (non-empty flow_override,
// lowest inbound_id first — same rule as EffectiveFlow) for many clients in one
// query, keyed by email. Emails absent from the result carry no flow anywhere.
// Batched so flow restoration on an inbound with many clients is O(1) queries
// instead of O(clients). Used to restore a stripped flow onto an inbound that
// has just become flow-eligible.
func (s *ClientService) EffectiveFlowsByEmails(tx *gorm.DB, emails []string) (map[string]string, error) {
	if tx == nil {
		tx = database.GetDB()
	}
	out := make(map[string]string, len(emails))
	if len(emails) == 0 {
		return out, nil
	}
	type row struct {
		Email string
		Flow  string `gorm:"column:flow_override"`
	}
	for _, batch := range chunkStrings(emails, sqlInChunk) {
		var rows []row
		err := tx.Table("client_inbounds").
			Select("clients.email AS email, client_inbounds.flow_override AS flow_override").
			Joins("JOIN clients ON clients.id = client_inbounds.client_id").
			Where("clients.email IN ? AND client_inbounds.flow_override <> ?", batch, "").
			Order("client_inbounds.inbound_id ASC").
			Scan(&rows).Error
		if err != nil {
			return nil, err
		}
		for _, r := range rows {
			if _, seen := out[r.Email]; !seen { // ordered by inbound_id ASC → first = lowest
				out[r.Email] = r.Flow
			}
		}
	}
	return out, nil
}

func (s *ClientService) GetInboundIdsForEmail(tx *gorm.DB, email string) ([]int, error) {
	if tx == nil {
		tx = database.GetDB()
	}
	var ids []int
	err := tx.Table("client_inbounds").
		Select("client_inbounds.inbound_id").
		Joins("JOIN clients ON clients.id = client_inbounds.client_id").
		Where("clients.email = ?", email).
		Scan(&ids).Error
	if err != nil {
		return nil, err
	}
	return ids, nil
}

func (s *ClientService) GetRecordsByTgID(tgId int64) ([]*model.ClientRecord, error) {
	if tgId <= 0 {
		return nil, errors.New("tg_id must be a positive integer")
	}
	var rows []*model.ClientRecord
	err := database.GetDB().Where("tg_id = ?", tgId).Find(&rows).Error
	return rows, err
}

func (s *ClientService) GetByID(id int) (*model.ClientRecord, error) {
	row := &model.ClientRecord{}
	if err := database.GetDB().Where("id = ?", id).First(row).Error; err != nil {
		return nil, err
	}
	return row, nil
}

func (s *ClientService) GetInboundIdsForRecord(id int) ([]int, error) {
	var ids []int
	err := database.GetDB().Table("client_inbounds").
		Where("client_id = ?", id).
		Order("inbound_id ASC").
		Pluck("inbound_id", &ids).Error
	if err != nil {
		return nil, err
	}
	return ids, nil
}

func (s *ClientService) List() ([]ClientWithAttachments, error) {
	db := database.GetDB()
	var rows []model.ClientRecord
	if err := db.Order("id ASC").Find(&rows).Error; err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return []ClientWithAttachments{}, nil
	}

	clientIds := make([]int, 0, len(rows))
	emails := make([]string, 0, len(rows))
	for i := range rows {
		clientIds = append(clientIds, rows[i].Id)
		if rows[i].Email != "" {
			emails = append(emails, rows[i].Email)
		}
	}

	attachments := make(map[int][]int, len(rows))
	for _, batch := range chunkInts(clientIds, sqlInChunk) {
		var links []model.ClientInbound
		if err := db.Where("client_id IN ?", batch).Find(&links).Error; err != nil {
			return nil, err
		}
		for _, l := range links {
			attachments[l.ClientId] = append(attachments[l.ClientId], l.InboundId)
		}
	}

	trafficByEmail := make(map[string]*xray.ClientTraffic, len(emails))
	if len(emails) > 0 {
		var stats []xray.ClientTraffic
		for _, batch := range chunkStrings(emails, sqlInChunk) {
			var batchStats []xray.ClientTraffic
			if err := db.Where("email IN ?", batch).Find(&batchStats).Error; err != nil {
				return nil, err
			}
			stats = append(stats, batchStats...)
		}
		overlayGlobalTrafficValues(db, stats)
		for i := range stats {
			trafficByEmail[stats[i].Email] = &stats[i]
		}
	}

	out := make([]ClientWithAttachments, 0, len(rows))
	for i := range rows {
		out = append(out, ClientWithAttachments{
			ClientRecord: rows[i],
			InboundIds:   attachments[rows[i].Id],
			Traffic:      trafficByEmail[rows[i].Email],
		})
	}
	return out, nil
}

func (s *ClientService) HasPendingNode(inboundSvc *InboundService, email string) bool {
	if strings.TrimSpace(email) == "" {
		return false
	}
	ids, err := s.GetInboundIdsForEmail(nil, email)
	if err != nil {
		return false
	}
	return inboundSvc.AnyNodePending(ids)
}

// findInboundIdsByClientEmail returns every inbound whose settings.clients[]
// JSON contains an entry with the given email. Driver-portable (no JSON
// operators) by parsing in Go — fine for the rare fallback path.
func (s *ClientService) findInboundIdsByClientEmail(email string) ([]int, error) {
	var inbounds []model.Inbound
	if err := database.GetDB().
		Select("id, settings").
		Where("settings LIKE ?", "%"+email+"%").
		Find(&inbounds).Error; err != nil {
		return nil, err
	}
	out := make([]int, 0, len(inbounds))
	for _, ib := range inbounds {
		var settings map[string]any
		if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
			continue
		}
		clients, ok := settings["clients"].([]any)
		if !ok {
			continue
		}
		for _, c := range clients {
			cm, ok := c.(map[string]any)
			if !ok {
				continue
			}
			if cEmail, _ := cm["email"].(string); cEmail == email {
				out = append(out, ib.Id)
				break
			}
		}
	}
	return out, nil
}
