package commerce

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

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

type TicketCreateRequest struct {
	DepartmentID int64            `json:"department_id"`
	Subject      string           `json:"subject"`
	Content      string           `json:"content"`
	Priority     string           `json:"priority"`
	InstanceID   *int64           `json:"instance_id"`
	Attachments  []map[string]any `json:"attachments"`
}

type TicketReplyRequest struct {
	Content     string           `json:"content"`
	Attachments []map[string]any `json:"attachments"`
}

type TicketRatingRequest struct {
	Rating  int     `json:"rating"`
	Comment *string `json:"comment"`
}

var activeTicketStatuses = map[string]struct{}{
	"open":         {},
	"pending":      {},
	"processing":   {},
	"waiting_user": {},
	"answered":     {},
}

func (s *Service) ListTicketDepartments(ctx context.Context) ([]map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var departments []TicketDepartment
	if err := s.db.WithContext(ctx).Where("is_enabled = ?", true).Order("sort_order ASC, id ASC").Find(&departments).Error; err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(departments))
	for _, item := range departments {
		out = append(out, ticketDepartmentMap(item))
	}
	return out, nil
}

func (s *Service) ListTicketInstances(ctx context.Context, userID int64) ([]InstanceResponse, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var instances []Instance
	if err := s.db.WithContext(ctx).
		Where("user_id = ?", userID).
		Scopes(ticketAttachableInstanceScope).
		Order("id DESC").
		Find(&instances).Error; err != nil {
		return nil, err
	}
	products, groups, err := s.productsAndGroupsForInstances(ctx, instances)
	if err != nil {
		return nil, err
	}
	out := make([]InstanceResponse, 0, len(instances))
	for _, instance := range instances {
		product := products[instance.ProductID]
		var group *ProductGroup
		if product != nil {
			group = groups[product.GroupID]
		}
		out = append(out, s.newInstanceResponse(instance, product, group, nil, false))
	}
	return out, nil
}

func (s *Service) ListTickets(ctx context.Context, userID int64, status string, limit int, offset int) ([]map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	db := s.db.WithContext(ctx).Where("user_id = ?", userID)
	if strings.TrimSpace(status) != "" {
		db = db.Where("status = ?", strings.TrimSpace(status))
	}
	var tickets []Ticket
	if err := db.Order("last_message_at DESC, id DESC").Limit(limit).Offset(offset).Find(&tickets).Error; err != nil {
		return nil, err
	}
	counts, err := s.ticketMessageCounts(ctx, ticketIDs(tickets))
	if err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(tickets))
	for _, ticket := range tickets {
		out = append(out, s.ticketSummary(ctx, ticket, counts[ticket.ID]))
	}
	return out, nil
}

func (s *Service) CreateTicket(ctx context.Context, userID int64, req TicketCreateRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	subject := strings.TrimSpace(req.Subject)
	content := strings.TrimSpace(req.Content)
	if len([]rune(subject)) < 2 || len([]rune(subject)) > 160 {
		return nil, BadRequest("ticket subject length must be 2-160")
	}
	if len([]rune(content)) < 2 || len([]rune(content)) > 8000 {
		return nil, BadRequest("ticket content length must be 2-8000")
	}
	priority := normalizeTicketPriority(req.Priority)

	var out map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		var department TicketDepartment
		err := tx.Where("id = ? AND is_enabled = ?", req.DepartmentID, true).First(&department).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return NotFound("ticket department not found")
		}
		if err != nil {
			return err
		}
		if req.InstanceID != nil {
			if err := txService.ensureUserInstance(ctx, userID, *req.InstanceID); err != nil {
				return err
			}
		}

		now := time.Now().UTC()
		ticket := Ticket{
			TicketNo:       txService.newTicketNo(ctx),
			UserID:         userID,
			DepartmentID:   &department.ID,
			DepartmentName: &department.Name,
			InstanceID:     req.InstanceID,
			Subject:        subject,
			Priority:       priority,
			Status:         "open",
			LastMessageAt:  &now,
			LastReplyBy:    stringPtr("user"),
		}
		if err := tx.Create(&ticket).Error; err != nil {
			return err
		}
		if err := txService.addTicketEvent(ctx, ticket.ID, "user", &userID, "created", "", "open", "User created ticket", map[string]any{"subject": subject, "department_name": department.Name}); err != nil {
			return err
		}
		if _, err := txService.addTicketMessage(ctx, &ticket, "user", &userID, content, req.Attachments, "open"); err != nil {
			return err
		}
		if department.AutoReplyEnabled && department.AutoReplyContent != nil && strings.TrimSpace(*department.AutoReplyContent) != "" {
			if _, err := txService.addTicketMessage(ctx, &ticket, "system", nil, strings.TrimSpace(*department.AutoReplyContent), nil, "open"); err != nil {
				return err
			}
		}
		out = txService.ticketSummary(ctx, ticket, 0)
		return nil
	})
	return out, err
}

func (s *Service) GetTicket(ctx context.Context, userID int64, ticketID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var ticket Ticket
	err := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", ticketID, userID).First(&ticket).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, NotFound("ticket not found")
	}
	if err != nil {
		return nil, err
	}
	_ = s.db.WithContext(ctx).Model(&Ticket{}).Where("id = ?", ticket.ID).Update("user_unread_count", 0).Error
	ticket.UserUnreadCount = 0

	var messages []TicketMessage
	if err := s.db.WithContext(ctx).Where("ticket_id = ? AND is_internal = ?", ticket.ID, false).Order("id ASC").Find(&messages).Error; err != nil {
		return nil, err
	}
	var events []TicketEvent
	if err := s.db.WithContext(ctx).Where("ticket_id = ?", ticket.ID).Order("id ASC").Find(&events).Error; err != nil {
		return nil, err
	}
	data := s.ticketSummary(ctx, ticket, len(messages))
	data["messages"] = ticketMessageMaps(messages)
	data["events"] = ticketEventMaps(events)
	return data, nil
}

func (s *Service) ReplyTicket(ctx context.Context, userID int64, ticketID int64, req TicketReplyRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	content := strings.TrimSpace(req.Content)
	if len([]rune(content)) < 1 || len([]rune(content)) > 8000 {
		return nil, BadRequest("ticket reply length must be 1-8000")
	}
	var out map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		ticket, err := txService.lockUserTicket(ctx, userID, ticketID)
		if err != nil {
			return err
		}
		if ticket.Status == "closed" || ticket.Status == "resolved" {
			return BadRequest("ticket is closed, reopen it before replying")
		}
		oldStatus := ticket.Status
		message, err := txService.addTicketMessage(ctx, &ticket, "user", &userID, content, req.Attachments, "pending")
		if err != nil {
			return err
		}
		if err := txService.addTicketEvent(ctx, ticket.ID, "user", &userID, "user_replied", oldStatus, "pending", "User replied", nil); err != nil {
			return err
		}
		out = ticketMessageMap(message)
		return nil
	})
	return out, err
}

func (s *Service) CloseTicket(ctx context.Context, userID int64, ticketID int64) (map[string]any, error) {
	return s.changeTicketStatus(ctx, userID, ticketID, "closed", "closed", "User closed ticket")
}

func (s *Service) ReopenTicket(ctx context.Context, userID int64, ticketID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var out map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		ticket, err := txService.lockUserTicket(ctx, userID, ticketID)
		if err != nil {
			return err
		}
		if _, ok := activeTicketStatuses[ticket.Status]; ok {
			out = txService.ticketSummary(ctx, ticket, 0)
			return nil
		}
		oldStatus := ticket.Status
		if _, err := txService.addTicketMessage(ctx, &ticket, "system", nil, "User reopened ticket.", nil, "open"); err != nil {
			return err
		}
		if err := txService.addTicketEvent(ctx, ticket.ID, "user", &userID, "reopened", oldStatus, "open", "User reopened ticket", nil); err != nil {
			return err
		}
		ticket.ClosedAt = nil
		out = txService.ticketSummary(ctx, ticket, 0)
		return nil
	})
	return out, err
}

func (s *Service) RateTicket(ctx context.Context, userID int64, ticketID int64, req TicketRatingRequest) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if req.Rating < 1 || req.Rating > 5 {
		return nil, BadRequest("rating must be 1-5")
	}
	var out map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		ticket, err := txService.lockUserTicket(ctx, userID, ticketID)
		if err != nil {
			return err
		}
		if ticket.Status != "closed" && ticket.Status != "resolved" {
			return BadRequest("ticket can only be rated after closed or resolved")
		}
		now := time.Now().UTC()
		oldRating := ""
		if ticket.SatisfactionRating != nil {
			oldRating = strconvItoa(*ticket.SatisfactionRating)
		}
		comment := trimTicketComment(req.Comment)
		if err := tx.Model(&Ticket{}).Where("id = ?", ticket.ID).Updates(map[string]any{
			"satisfaction_rating":   req.Rating,
			"satisfaction_comment":  comment,
			"satisfaction_rated_at": now,
		}).Error; err != nil {
			return err
		}
		ticket.SatisfactionRating = &req.Rating
		ticket.SatisfactionComment = comment
		ticket.SatisfactionRatedAt = &now
		if err := txService.addTicketEvent(ctx, ticket.ID, "user", &userID, "rated", oldRating, strconvItoa(req.Rating), "User rated ticket", map[string]any{"rating": req.Rating, "has_comment": comment != nil}); err != nil {
			return err
		}
		out = txService.ticketSummary(ctx, ticket, 0)
		return nil
	})
	return out, err
}

func (s *Service) changeTicketStatus(ctx context.Context, userID int64, ticketID int64, status string, eventType string, message string) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var out map[string]any
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		txService := &Service{db: tx, logger: s.logger}
		ticket, err := txService.lockUserTicket(ctx, userID, ticketID)
		if err != nil {
			return err
		}
		oldStatus := ticket.Status
		now := time.Now().UTC()
		updates := map[string]any{"status": status}
		if status == "closed" {
			updates["closed_at"] = now
			ticket.ClosedAt = &now
		}
		if err := tx.Model(&Ticket{}).Where("id = ?", ticket.ID).Updates(updates).Error; err != nil {
			return err
		}
		ticket.Status = status
		if err := txService.addTicketEvent(ctx, ticket.ID, "user", &userID, eventType, oldStatus, status, message, nil); err != nil {
			return err
		}
		out = txService.ticketSummary(ctx, ticket, 0)
		return nil
	})
	return out, err
}

func (s *Service) lockUserTicket(ctx context.Context, userID int64, ticketID int64) (Ticket, error) {
	var ticket Ticket
	err := s.db.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", ticketID, userID).First(&ticket).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return Ticket{}, NotFound("ticket not found")
	}
	return ticket, err
}

func (s *Service) ensureUserInstance(ctx context.Context, userID int64, instanceID int64) error {
	var count int64
	if err := s.db.WithContext(ctx).Model(&Instance{}).
		Where("id = ? AND user_id = ?", instanceID, userID).
		Scopes(ticketAttachableInstanceScope).
		Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return NotFound("instance not found")
	}
	return nil
}

func (s *Service) addTicketMessage(ctx context.Context, ticket *Ticket, senderType string, senderID *int64, content string, attachments []map[string]any, nextStatus string) (TicketMessage, error) {
	return s.addTicketMessageWithOptions(ctx, ticket, senderType, senderID, content, attachments, nextStatus, false)
}

func (s *Service) addTicketEvent(ctx context.Context, ticketID int64, actorType string, actorID *int64, eventType string, from string, to string, message string, meta map[string]any) error {
	var fromPtr *string
	if from != "" {
		fromPtr = &from
	}
	var toPtr *string
	if to != "" {
		toPtr = &to
	}
	var messagePtr *string
	if message != "" {
		messagePtr = &message
	}
	rawMeta, _ := json.Marshal(meta)
	event := TicketEvent{
		TicketID:  ticketID,
		ActorType: actorType,
		ActorID:   actorID,
		EventType: eventType,
		FromValue: fromPtr,
		ToValue:   toPtr,
		Message:   messagePtr,
		Meta:      JSONRaw(rawMeta),
	}
	if meta == nil {
		event.Meta = nil
	}
	return s.db.WithContext(ctx).Create(&event).Error
}

func (s *Service) ticketMessageCounts(ctx context.Context, ids []int64) (map[int64]int, error) {
	out := map[int64]int{}
	if len(ids) == 0 {
		return out, nil
	}
	rows := []struct {
		TicketID int64
		Count    int
	}{}
	if err := s.db.WithContext(ctx).Model(&TicketMessage{}).Select("ticket_id, COUNT(*) as count").Where("ticket_id IN ?", ids).Group("ticket_id").Scan(&rows).Error; err != nil {
		return nil, err
	}
	for _, row := range rows {
		out[row.TicketID] = row.Count
	}
	return out, nil
}

func (s *Service) newTicketNo(ctx context.Context) string {
	prefix := time.Now().UTC().Format("T20060102")
	for i := 0; i < 10; i++ {
		var raw [3]byte
		_, _ = rand.Read(raw[:])
		no := prefix + strings.ToUpper(hex.EncodeToString(raw[:]))
		var count int64
		if err := s.db.WithContext(ctx).Model(&Ticket{}).Where("ticket_no = ?", no).Count(&count).Error; err == nil && count == 0 {
			return no
		}
	}
	return fmt.Sprintf("%s%d", prefix, time.Now().UTC().UnixNano())
}

func (s *Service) ticketSummary(ctx context.Context, ticket Ticket, messageCount int) map[string]any {
	if messageCount == 0 && ticket.ID > 0 {
		counts, _ := s.ticketMessageCounts(ctx, []int64{ticket.ID})
		messageCount = counts[ticket.ID]
	}
	return map[string]any{
		"id":              ticket.ID,
		"ticket_no":       ticket.TicketNo,
		"subject":         ticket.Subject,
		"status":          ticket.Status,
		"priority":        ticket.Priority,
		"department_id":   ticket.DepartmentID,
		"department_name": ticket.DepartmentName,
		"instance":        s.ticketInstance(ctx, ticket.InstanceID, ticket.UserID),
		"assignee":        nil,
		"assigned_at":     ticket.AssignedAt,
		"last_message_at": ticket.LastMessageAt,
		"last_reply_by":   ticket.LastReplyBy,
		"unread_count":    ticket.UserUnreadCount,
		"message_count":   messageCount,
		"satisfaction":    ticketSatisfaction(ticket),
		"created_at":      ticket.CreatedAt,
		"updated_at":      ticket.UpdatedAt,
	}
}

func (s *Service) ticketInstance(ctx context.Context, instanceID *int64, userID int64) any {
	if instanceID == nil {
		return nil
	}
	var instance Instance
	err := s.db.WithContext(ctx).
		Where("id = ? AND user_id = ?", *instanceID, userID).
		Scopes(ticketAttachableInstanceScope).
		First(&instance).Error
	if err != nil {
		return nil
	}
	return map[string]any{
		"id":         instance.ID,
		"status":     instance.Status,
		"hostname":   instance.Hostname,
		"main_ip":    instance.MainIP,
		"cpu":        instance.CPU,
		"memory":     instance.Memory,
		"in_bw":      instance.InBW,
		"out_bw":     instance.OutBW,
		"expires_at": instance.ExpiresAt,
	}
}

func ticketIDs(tickets []Ticket) []int64 {
	out := make([]int64, 0, len(tickets))
	for _, item := range tickets {
		out = append(out, item.ID)
	}
	return out
}

func ticketDepartmentMap(item TicketDepartment) map[string]any {
	return map[string]any{
		"id":                 item.ID,
		"code":               item.Code,
		"name":               item.Name,
		"description":        item.Description,
		"is_enabled":         item.IsEnabled,
		"sort_order":         item.SortOrder,
		"min_member_level":   item.MinMemberLevel,
		"auto_reply_enabled": item.AutoReplyEnabled,
		"auto_reply_content": item.AutoReplyContent,
		"created_at":         item.CreatedAt,
		"updated_at":         item.UpdatedAt,
	}
}

func ticketMessageMaps(messages []TicketMessage) []map[string]any {
	out := make([]map[string]any, 0, len(messages))
	for _, item := range messages {
		out = append(out, ticketMessageMap(item))
	}
	return out
}

func ticketMessageMap(item TicketMessage) map[string]any {
	return map[string]any{
		"id":          item.ID,
		"ticket_id":   item.TicketID,
		"sender_type": item.SenderType,
		"sender_id":   item.SenderID,
		"sender_name": nil,
		"content":     item.Content,
		"attachments": jsonList(item.Attachments),
		"is_internal": item.IsInternal,
		"created_at":  item.CreatedAt,
	}
}

func ticketEventMaps(events []TicketEvent) []map[string]any {
	out := make([]map[string]any, 0, len(events))
	for _, item := range events {
		out = append(out, map[string]any{
			"id":         item.ID,
			"ticket_id":  item.TicketID,
			"actor_type": item.ActorType,
			"actor_id":   item.ActorID,
			"event_type": item.EventType,
			"from_value": item.FromValue,
			"to_value":   item.ToValue,
			"message":    item.Message,
			"meta":       jsonMap(item.Meta),
			"created_at": item.CreatedAt,
		})
	}
	return out
}

func ticketSatisfaction(ticket Ticket) any {
	if ticket.SatisfactionRating == nil {
		return nil
	}
	return map[string]any{
		"rating":   ticket.SatisfactionRating,
		"comment":  ticket.SatisfactionComment,
		"rated_at": ticket.SatisfactionRatedAt,
	}
}

func normalizeTicketPriority(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "low", "high", "urgent":
		return strings.ToLower(strings.TrimSpace(value))
	default:
		return "normal"
	}
}

func trimTicketComment(value *string) *string {
	if value == nil {
		return nil
	}
	text := strings.TrimSpace(*value)
	if text == "" {
		return nil
	}
	if len([]rune(text)) > 1000 {
		runes := []rune(text)
		text = string(runes[:1000])
	}
	return &text
}

func boolInt(value bool) int {
	if value {
		return 1
	}
	return 0
}

func strconvItoa(value int) string {
	return fmt.Sprintf("%d", value)
}
