package kanban

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"tm/pkg/gorules"
	"tm/pkg/logger"
)

// KanbanService interface defines kanban business logic operations
type Service interface {
	// Board operations
	CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error)
	GetBoardByID(ctx context.Context, id, ownerID string) (*BoardDetailResponse, error)
	GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error)
	GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error)
	UpdateBoard(ctx context.Context, req UpdateBoardRequest, ownerID string) (*BoardResponse, error)
	DeleteBoard(ctx context.Context, id, ownerID string) error

	// Column operations
	CreateColumn(ctx context.Context, req CreateColumnRequest, ownerID string) (*ColumnResponse, error)
	UpdateColumn(ctx context.Context, req UpdateColumnRequest, ownerID string) (*ColumnResponse, error)
	DeleteColumn(ctx context.Context, id, ownerID string) error
	ReorderColumns(ctx context.Context, req ReorderColumnsRequest, ownerID string) error

	// Card operations
	CreateCard(ctx context.Context, req CreateCardRequest, ownerID string) (*CardResponse, error)
	GetCardByID(ctx context.Context, id, ownerID string) (*CardResponse, error)
	UpdateCard(ctx context.Context, req UpdateCardRequest, ownerID string) (*CardResponse, error)
	DeleteCard(ctx context.Context, id, ownerID string) error
	MoveCard(ctx context.Context, req MoveCardRequest, ownerID string) error
	ReorderCards(ctx context.Context, req ReorderCardsRequest, ownerID string) error
}

// kanbanService implements KanbanService interface
type kanbanService struct {
	boardRepository  BoardRepository
	columnRepository ColumnRepository
	cardRepository   CardRepository
	goRulesClient    gorules.Client
	goRulesRuleID    string
	logger           logger.Logger
}

// NewService creates a new kanban service
func NewService(boardRepo BoardRepository, columnRepo ColumnRepository, cardRepo CardRepository, goRulesClient gorules.Client, goRulesRuleID string, logger logger.Logger) Service {
	return &kanbanService{
		boardRepository:  boardRepo,
		columnRepository: columnRepo,
		cardRepository:   cardRepo,
		goRulesClient:    goRulesClient,
		goRulesRuleID:    goRulesRuleID,
		logger:           logger,
	}
}

var defaultColumnColors = []string{"#64748B", "#0EA5E9", "#22C55E", "#A855F7", "#F59E0B"}

// CreateBoard creates a new kanban board
func (s *kanbanService) CreateBoard(ctx context.Context, req CreateBoardRequest) (*BoardResponse, error) {
	s.logger.Info("Creating new board", map[string]interface{}{
		"name":       req.Name,
		"user_id":    req.UserID,
		"is_default": req.IsDefault,
	})

	// If this is set as default, unset any existing default board for this user
	if req.IsDefault {
		if err := s.unsetDefaultBoard(ctx, req.UserID); err != nil {
			s.logger.Warn("Failed to unset existing default board", map[string]interface{}{
				"user_id": req.UserID,
				"error":   err.Error(),
			})
		}
	}

	board := &Board{
		Name:        req.Name,
		Description: req.Description,
		IsDefault:   req.IsDefault,
		UserID:      req.UserID,
		Columns:     []Column{}, // Start with empty columns
	}

	if err := s.boardRepository.CreateBoard(ctx, board); err != nil {
		return nil, err
	}

	if err := s.ensureDefaultColumns(ctx, board.ID.Hex()); err != nil {
		return nil, err
	}

	return board.ToResponse(), nil
}

// GetBoardByID retrieves a board with full column and card details
func (s *kanbanService) GetBoardByID(ctx context.Context, id, ownerID string) (*BoardDetailResponse, error) {
	s.logger.Info("Retrieving board by ID", map[string]interface{}{
		"board_id": id,
		"owner_id": ownerID,
	})

	board, err := s.getBoardForOwner(ctx, id, ownerID)
	if err != nil {
		return nil, err
	}

	if err := s.ensureDefaultColumns(ctx, id); err != nil {
		return nil, err
	}

	boardResponse := board.ToResponse()

	// Load columns with cards
	columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id)
	if err != nil {
		return nil, err
	}

	for _, column := range columns {
		columnResponse := column.ToResponse()

		// Load cards for this column
		cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex())
		if err != nil {
			s.logger.Warn("Failed to load cards for column", map[string]interface{}{
				"column_id": column.ID.Hex(),
				"error":     err.Error(),
			})
			continue
		}

		columnResponse.CardCount = len(cards)
		if column.Limit != nil && len(cards) > *column.Limit {
			columnResponse.IsOverLimit = true
		}

		for _, card := range cards {
			columnResponse.Cards = append(columnResponse.Cards, *card.ToResponse())
		}

		boardResponse.Columns = append(boardResponse.Columns, *columnResponse)
	}

	return &BoardDetailResponse{
		Board: *boardResponse,
	}, nil
}

// GetBoardsByUserID retrieves all boards for a user
func (s *kanbanService) GetBoardsByUserID(ctx context.Context, userID string) (*BoardsListResponse, error) {
	s.logger.Info("Retrieving boards by user ID", map[string]interface{}{
		"user_id": userID,
	})

	boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}

	var boardResponses []BoardResponse
	for _, board := range boards {
		boardResponses = append(boardResponses, *board.ToResponse())
	}

	return &BoardsListResponse{
		Boards: boardResponses,
	}, nil
}

// GetDefaultBoard retrieves the default board for a user
func (s *kanbanService) GetDefaultBoard(ctx context.Context, userID string) (*BoardDetailResponse, error) {
	s.logger.Info("Retrieving default board", map[string]interface{}{
		"user_id": userID,
	})

	board, err := s.boardRepository.GetDefaultBoard(ctx, userID)
	if err != nil {
		return nil, err
	}

	return s.GetBoardByID(ctx, board.ID.Hex(), userID)
}

// UpdateBoard updates an existing board
func (s *kanbanService) UpdateBoard(ctx context.Context, req UpdateBoardRequest, ownerID string) (*BoardResponse, error) {
	s.logger.Info("Updating board", map[string]interface{}{
		"board_id": req.ID,
		"owner_id": ownerID,
	})

	board, err := s.getBoardForOwner(ctx, req.ID, ownerID)
	if err != nil {
		return nil, err
	}

	// Update fields
	if req.Name != nil {
		board.Name = *req.Name
	}
	if req.Description != nil {
		board.Description = *req.Description
	}
	if req.IsDefault != nil {
		// If setting as default, unset other default boards
		if *req.IsDefault && !board.IsDefault {
			if err := s.unsetDefaultBoard(ctx, board.UserID); err != nil {
				s.logger.Warn("Failed to unset existing default board", map[string]interface{}{
					"user_id": board.UserID,
					"error":   err.Error(),
				})
			}
		}
		board.IsDefault = *req.IsDefault
	}

	if err := s.boardRepository.UpdateBoard(ctx, board); err != nil {
		return nil, err
	}

	return board.ToResponse(), nil
}

// DeleteBoard deletes a board and all its columns and cards
func (s *kanbanService) DeleteBoard(ctx context.Context, id, ownerID string) error {
	s.logger.Info("Deleting board", map[string]interface{}{
		"board_id": id,
		"owner_id": ownerID,
	})

	if _, err := s.getBoardForOwner(ctx, id, ownerID); err != nil {
		return err
	}

	// Get all columns for this board
	columns, err := s.columnRepository.GetColumnsByBoardID(ctx, id)
	if err != nil {
		return err
	}

	// Delete all cards in each column
	for _, column := range columns {
		cards, err := s.cardRepository.GetCardsByColumnID(ctx, column.ID.Hex())
		if err != nil {
			s.logger.Warn("Failed to get cards for column during board deletion", map[string]interface{}{
				"column_id": column.ID.Hex(),
				"error":     err.Error(),
			})
			continue
		}

		for _, card := range cards {
			if err := s.cardRepository.DeleteCard(ctx, card.ID.Hex()); err != nil {
				s.logger.Warn("Failed to delete card during board deletion", map[string]interface{}{
					"card_id": card.ID.Hex(),
					"error":   err.Error(),
				})
			}
		}

		// Delete the column
		if err := s.columnRepository.DeleteColumn(ctx, column.ID.Hex()); err != nil {
			s.logger.Warn("Failed to delete column during board deletion", map[string]interface{}{
				"column_id": column.ID.Hex(),
				"error":     err.Error(),
			})
		}
	}

	// Finally delete the board
	return s.boardRepository.DeleteBoard(ctx, id)
}

// CreateColumn creates a new column in a board
func (s *kanbanService) CreateColumn(ctx context.Context, req CreateColumnRequest, ownerID string) (*ColumnResponse, error) {
	s.logger.Info("Creating new column", map[string]interface{}{
		"name":     req.Name,
		"board_id": req.BoardID,
		"order":    req.Order,
	})

	if _, err := s.getBoardForOwner(ctx, req.BoardID, ownerID); err != nil {
		return nil, err
	}

	return nil, fmt.Errorf("kanban board uses a fixed bid workflow; columns are created automatically from GoRules")
}

// UpdateColumn updates an existing column
func (s *kanbanService) UpdateColumn(ctx context.Context, req UpdateColumnRequest, ownerID string) (*ColumnResponse, error) {
	s.logger.Info("Updating column", map[string]interface{}{
		"column_id": req.ID,
		"owner_id":  ownerID,
	})

	column, _, err := s.getColumnForOwner(ctx, req.ID, ownerID)
	if err != nil {
		return nil, err
	}

	if req.Name != nil {
		return nil, fmt.Errorf("column name cannot be changed on a fixed workflow board")
	}
	if req.Order != nil {
		return nil, fmt.Errorf("column order cannot be changed on a fixed workflow board")
	}
	if req.Limit != nil {
		return nil, fmt.Errorf("column WIP limit cannot be changed on a fixed workflow board")
	}
	if req.Color == nil {
		return nil, fmt.Errorf("only column color can be updated on a fixed workflow board")
	}

	column.Color = *req.Color

	if err := s.columnRepository.UpdateColumn(ctx, column); err != nil {
		return nil, err
	}

	return column.ToResponse(), nil
}

// DeleteColumn deletes a column and moves all its cards to the next column
func (s *kanbanService) DeleteColumn(ctx context.Context, id, ownerID string) error {
	s.logger.Info("Deleting column", map[string]interface{}{
		"column_id": id,
	})

	return fmt.Errorf("columns are fixed and cannot be deleted")
}

// ReorderColumns reorders columns within a board
func (s *kanbanService) ReorderColumns(ctx context.Context, req ReorderColumnsRequest, ownerID string) error {
	s.logger.Info("Reordering columns", map[string]interface{}{
		"board_id": req.BoardID,
		"count":    len(req.ColumnOrders),
	})

	if _, err := s.getBoardForOwner(ctx, req.BoardID, ownerID); err != nil {
		return err
	}

	return fmt.Errorf("columns are fixed and cannot be reordered")
}

// CreateCard creates a new card in a column
func (s *kanbanService) CreateCard(ctx context.Context, req CreateCardRequest, ownerID string) (*CardResponse, error) {
	s.logger.Info("Creating new card", map[string]interface{}{
		"title":     req.Title,
		"tender_id": req.TenderID,
		"column_id": req.ColumnID,
		"order":     req.Order,
		"owner_id":  ownerID,
	})

	if _, _, err := s.getColumnForOwner(ctx, req.ColumnID, ownerID); err != nil {
		return nil, err
	}

	// Check for duplicate card (same tender in same column)
	existingCards, err := s.cardRepository.GetCardsByTenderID(ctx, req.TenderID)
	if err != nil {
		return nil, err
	}

	for _, card := range existingCards {
		if card.ColumnID == req.ColumnID {
			return nil, fmt.Errorf("card for this tender already exists in the column")
		}
	}

	card := &Card{
		ColumnID: req.ColumnID,
		TenderID: req.TenderID,
		Title:    req.Title,
		Order:    req.Order,
		Priority: req.Priority,
		Labels:   req.Labels,
		DueDate:  req.DueDate,
	}

	if err := s.cardRepository.CreateCard(ctx, card); err != nil {
		return nil, err
	}

	return card.ToResponse(), nil
}

// GetCardByID retrieves a card by ID
func (s *kanbanService) GetCardByID(ctx context.Context, id, ownerID string) (*CardResponse, error) {
	s.logger.Info("Retrieving card by ID", map[string]interface{}{
		"card_id":  id,
		"owner_id": ownerID,
	})

	card, _, _, err := s.getCardForOwner(ctx, id, ownerID)
	if err != nil {
		return nil, err
	}

	return card.ToResponse(), nil
}

// UpdateCard updates an existing card
func (s *kanbanService) UpdateCard(ctx context.Context, req UpdateCardRequest, ownerID string) (*CardResponse, error) {
	s.logger.Info("Updating card", map[string]interface{}{
		"card_id":  req.ID,
		"owner_id": ownerID,
	})

	card, _, _, err := s.getCardForOwner(ctx, req.ID, ownerID)
	if err != nil {
		return nil, err
	}

	// Update fields
	if req.Title != nil {
		card.Title = *req.Title
	}
	if req.Priority != nil {
		card.Priority = *req.Priority
	}
	if req.Labels != nil {
		card.Labels = req.Labels
	}
	if req.DueDate != nil {
		card.DueDate = req.DueDate
	}

	if err := s.cardRepository.UpdateCard(ctx, card); err != nil {
		return nil, err
	}

	return card.ToResponse(), nil
}

// DeleteCard deletes a card
func (s *kanbanService) DeleteCard(ctx context.Context, id, ownerID string) error {
	s.logger.Info("Deleting card", map[string]interface{}{
		"card_id":  id,
		"owner_id": ownerID,
	})

	if _, _, _, err := s.getCardForOwner(ctx, id, ownerID); err != nil {
		return err
	}

	return s.cardRepository.DeleteCard(ctx, id)
}

// MoveCard moves a card to a different column
func (s *kanbanService) MoveCard(ctx context.Context, req MoveCardRequest, ownerID string) error {
	s.logger.Info("Moving card", map[string]interface{}{
		"card_id":   req.CardID,
		"column_id": req.ColumnID,
		"new_order": req.NewOrder,
		"owner_id":  ownerID,
	})

	card, currentColumn, board, err := s.getCardForOwner(ctx, req.CardID, ownerID)
	if err != nil {
		return err
	}

	targetColumn, _, err := s.getColumnForOwner(ctx, req.ColumnID, ownerID)
	if err != nil {
		return err
	}

	if targetColumn.BoardID != board.ID.Hex() {
		return ErrAccessDenied
	}

	allowedStatuses, err := s.availableStatuses(ctx, board.ID.Hex(), currentColumn.Status)
	if err != nil {
		return err
	}

	targetStatus := s.normalizeStatus(targetColumn.Status)
	if _, exists := allowedStatuses[targetStatus]; !exists {
		return fmt.Errorf("status transition from %q to %q is not allowed", currentColumn.Status, targetColumn.Status)
	}

	_ = card
	return s.cardRepository.MoveCard(ctx, req.CardID, req.ColumnID, req.NewOrder)
}

// ReorderCards reorders cards within a column
func (s *kanbanService) ReorderCards(ctx context.Context, req ReorderCardsRequest, ownerID string) error {
	s.logger.Info("Reordering cards", map[string]interface{}{
		"column_id": req.ColumnID,
		"count":     len(req.CardOrders),
		"owner_id":  ownerID,
	})

	if _, _, err := s.getColumnForOwner(ctx, req.ColumnID, ownerID); err != nil {
		return err
	}

	return s.cardRepository.UpdateCardOrder(ctx, req.ColumnID, req.CardOrders)
}

// unsetDefaultBoard removes the default flag from all boards for a user
func (s *kanbanService) unsetDefaultBoard(ctx context.Context, userID string) error {
	boards, err := s.boardRepository.GetBoardsByUserID(ctx, userID)
	if err != nil {
		return err
	}

	for _, board := range boards {
		if board.IsDefault {
			board.IsDefault = false
			if err := s.boardRepository.UpdateBoard(ctx, &board); err != nil {
				return err
			}
		}
	}

	return nil
}

func (s *kanbanService) ensureDefaultColumns(ctx context.Context, boardID string) error {
	columns, err := s.columnRepository.GetColumnsByBoardID(ctx, boardID)
	if err != nil {
		return err
	}
	if len(columns) > 0 {
		return nil
	}

	statuses, err := s.resolveWorkflowColumns(ctx)
	if err != nil {
		return err
	}

	for idx, status := range statuses {
		column := &Column{
			BoardID:        boardID,
			Name:           status.Status,
			Status:         status.Status,
			StatusCategory: status.StatusCategory,
			Color:          s.columnColor(status, idx),
			Order:          idx + 1,
		}

		if err := s.columnRepository.CreateColumn(ctx, column); err != nil {
			return err
		}
	}

	return nil
}

func (s *kanbanService) defaultBidWorkflowStatuses() []gorules.StatusWithCategory {
	return []gorules.StatusWithCategory{
		{Status: "Qualification", StatusCategory: "qualification"},
		{Status: "Proposal", StatusCategory: "proposal"},
		{Status: "Submission", StatusCategory: "submission"},
	}
}

func dedupeStatuses(statuses []gorules.StatusWithCategory) []gorules.StatusWithCategory {
	seen := make(map[string]struct{})
	out := make([]gorules.StatusWithCategory, 0, len(statuses))
	for _, item := range statuses {
		if strings.TrimSpace(item.Status) == "" {
			continue
		}
		key := strings.ToLower(strings.TrimSpace(item.Status))
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, item)
	}
	return out
}

// resolveWorkflowColumns returns column definitions to persist.
// When GoRules is configured, only a successful GoRules response is persisted.
// When GoRules is not configured, built-in bid workflow defaults are used (explicit offline mode).
func (s *kanbanService) resolveWorkflowColumns(ctx context.Context) ([]gorules.StatusWithCategory, error) {
	if !s.goRulesEnabled() {
		s.logger.Info("GoRules not configured; using built-in bid workflow columns", map[string]interface{}{})
		return s.defaultBidWorkflowStatuses(), nil
	}

	fromRules, err := s.goRulesClient.EvaluateAvailableStatuses(ctx, "", s.goRulesRuleID)
	if err != nil {
		s.logger.Warn("GoRules unavailable; workflow columns not persisted", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("%w: %v", ErrWorkflowUnavailable, err)
	}

	normalized := dedupeStatuses(fromRules)
	if len(normalized) == 0 {
		return nil, fmt.Errorf("%w: empty status list from rules engine", ErrWorkflowUnavailable)
	}

	sort.SliceStable(normalized, func(i, j int) bool {
		return s.statusRank(normalized[i].Status) < s.statusRank(normalized[j].Status)
	})

	return normalized, nil
}

// availableStatuses returns allowed target column statuses for a move.
// With GoRules configured, only statuses returned by the rules engine are allowed (strict).
// Without GoRules, a linear fallback allows the current stage and the next stage only.
func (s *kanbanService) availableStatuses(ctx context.Context, boardID, currentStatus string) (map[string]struct{}, error) {
	columns, err := s.columnRepository.GetColumnsByBoardID(ctx, boardID)
	if err != nil {
		return nil, err
	}

	boardStatuses := columnStatusSet(columns)

	if !s.goRulesEnabled() {
		s.logger.Warn("GoRules not configured; using linear workflow fallback for card moves (current + next stage only)", map[string]interface{}{
			"board_id":       boardID,
			"current_status": currentStatus,
		})
		return s.linearTransitionFallback(columns, currentStatus), nil
	}

	fromRules, err := s.goRulesClient.EvaluateAvailableStatuses(ctx, currentStatus, s.goRulesRuleID)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrWorkflowUnavailable, err)
	}

	allowed := map[string]struct{}{}
	for _, item := range fromRules {
		normalized := s.normalizeStatus(item.Status)
		if _, onBoard := boardStatuses[normalized]; onBoard {
			allowed[normalized] = struct{}{}
		}
	}

	if len(allowed) == 0 {
		return nil, fmt.Errorf("no allowed transitions from %q per workflow rules", currentStatus)
	}

	return allowed, nil
}

func (s *kanbanService) goRulesEnabled() bool {
	return s.goRulesClient != nil && strings.TrimSpace(s.goRulesRuleID) != ""
}

func columnStatusSet(columns []Column) map[string]struct{} {
	set := make(map[string]struct{}, len(columns))
	for _, col := range columns {
		set[strings.ToLower(strings.TrimSpace(col.Status))] = struct{}{}
	}
	return set
}

// linearTransitionFallback allows staying on the current stage or moving to the next ordered column only.
func (s *kanbanService) linearTransitionFallback(columns []Column, currentStatus string) map[string]struct{} {
	sorted := append([]Column(nil), columns...)
	sort.Slice(sorted, func(i, j int) bool {
		return sorted[i].Order < sorted[j].Order
	})

	current := s.normalizeStatus(currentStatus)
	allowed := map[string]struct{}{current: {}}

	for i, col := range sorted {
		if s.normalizeStatus(col.Status) != current {
			continue
		}
		if i+1 < len(sorted) {
			allowed[s.normalizeStatus(sorted[i+1].Status)] = struct{}{}
		}
		break
	}

	return allowed
}

func (s *kanbanService) getBoardForOwner(ctx context.Context, boardID, ownerID string) (*Board, error) {
	board, err := s.boardRepository.GetBoardByID(ctx, boardID)
	if err != nil {
		return nil, err
	}
	if board.UserID != ownerID {
		return nil, ErrAccessDenied
	}
	return board, nil
}

func (s *kanbanService) getColumnForOwner(ctx context.Context, columnID, ownerID string) (*Column, *Board, error) {
	column, err := s.columnRepository.GetColumnByID(ctx, columnID)
	if err != nil {
		return nil, nil, err
	}
	board, err := s.getBoardForOwner(ctx, column.BoardID, ownerID)
	if err != nil {
		return nil, nil, err
	}
	return column, board, nil
}

func (s *kanbanService) getCardForOwner(ctx context.Context, cardID, ownerID string) (*Card, *Column, *Board, error) {
	card, err := s.cardRepository.GetCardByID(ctx, cardID)
	if err != nil {
		return nil, nil, nil, err
	}
	column, board, err := s.getColumnForOwner(ctx, card.ColumnID, ownerID)
	if err != nil {
		return nil, nil, nil, err
	}
	return card, column, board, nil
}

func (s *kanbanService) normalizeStatus(status string) string {
	return strings.ToLower(strings.TrimSpace(status))
}

func (s *kanbanService) statusRank(status string) int {
	switch s.normalizeStatus(status) {
	case "qualification", "to do", "identification":
		return 1
	case "proposal", "in progress":
		return 2
	case "submission", "done", "analysis":
		return 3
	default:
		return 99
	}
}

func (s *kanbanService) columnColor(status gorules.StatusWithCategory, index int) string {
	byCategory := map[string]string{
		"qualification": "#64748B",
		"to_do":         "#64748B",
		"proposal":      "#0EA5E9",
		"in_progress":   "#0EA5E9",
		"submission":    "#22C55E",
		"done":          "#22C55E",
		"analysis":      "#22C55E",
	}
	if c, ok := byCategory[strings.ToLower(strings.TrimSpace(status.StatusCategory))]; ok {
		return c
	}
	if index >= 0 && index < len(defaultColumnColors) {
		return defaultColumnColors[index]
	}
	return defaultColumnColors[0]
}
