package dash

import (
	"context"
	"fmt"
	"io"
	"path"
	"strings"
	"time"

	"github.com/seaweedfs/seaweedfs/weed/cluster"
	"github.com/seaweedfs/seaweedfs/weed/filer"
	"github.com/seaweedfs/seaweedfs/weed/glog"
	"github.com/seaweedfs/seaweedfs/weed/mq/topic"
	"github.com/seaweedfs/seaweedfs/weed/pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/mq_pb"
	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
	"github.com/seaweedfs/seaweedfs/weed/util"
)

// GetTopics retrieves message queue topics data
func (s *AdminServer) GetTopics() (*TopicsData, error) {
	var topics []TopicInfo

	// Find broker leader and get topics
	brokerLeader, err := s.findBrokerLeader()
	if err != nil {
		// If no broker leader found, return empty data
		return &TopicsData{
			Topics:      topics,
			TotalTopics: len(topics),
			LastUpdated: time.Now(),
		}, nil
	}

	// Connect to broker leader and list topics
	err = s.withBrokerClient(brokerLeader, func(client mq_pb.SeaweedMessagingClient) error {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		resp, err := client.ListTopics(ctx, &mq_pb.ListTopicsRequest{})
		if err != nil {
			return err
		}

		// Convert protobuf topics to TopicInfo - only include available data
		for _, pbTopic := range resp.Topics {
			topicInfo := TopicInfo{
				Name:       fmt.Sprintf("%s.%s", pbTopic.Namespace, pbTopic.Name),
				Partitions: 0, // Will be populated by LookupTopicBrokers call
				Retention: TopicRetentionInfo{
					Enabled:      false,
					DisplayValue: 0,
					DisplayUnit:  "days",
				},
			}

			// Get topic configuration to get partition count and retention info
			lookupResp, err := client.LookupTopicBrokers(ctx, &mq_pb.LookupTopicBrokersRequest{
				Topic: pbTopic,
			})
			if err == nil {
				topicInfo.Partitions = len(lookupResp.BrokerPartitionAssignments)
			}

			// Get topic configuration for retention information
			configResp, err := client.GetTopicConfiguration(ctx, &mq_pb.GetTopicConfigurationRequest{
				Topic: pbTopic,
			})
			if err == nil && configResp.Retention != nil {
				topicInfo.Retention = convertTopicRetention(configResp.Retention)
			}

			topics = append(topics, topicInfo)
		}

		return nil
	})

	if err != nil {
		// If connection fails, return empty data
		return &TopicsData{
			Topics:      topics,
			TotalTopics: len(topics),
			LastUpdated: time.Now(),
		}, nil
	}

	return &TopicsData{
		Topics:      topics,
		TotalTopics: len(topics),
		LastUpdated: time.Now(),
		// Don't include TotalMessages and TotalSize as they're not available
	}, nil
}

// GetSubscribers retrieves message queue subscribers data
func (s *AdminServer) GetSubscribers() (*SubscribersData, error) {
	var subscribers []SubscriberInfo

	// Find broker leader and get subscriber info from broker stats
	brokerLeader, err := s.findBrokerLeader()
	if err != nil {
		// If no broker leader found, return empty data
		return &SubscribersData{
			Subscribers:       subscribers,
			TotalSubscribers:  len(subscribers),
			ActiveSubscribers: 0,
			LastUpdated:       time.Now(),
		}, nil
	}

	// Connect to broker leader and get subscriber information
	// Note: SeaweedMQ doesn't have a direct API to list all subscribers
	// We would need to collect this information from broker statistics
	// For now, return empty data structure as subscriber info is not
	// directly available through the current MQ API
	err = s.withBrokerClient(brokerLeader, func(client mq_pb.SeaweedMessagingClient) error {
		// TODO: Implement subscriber data collection from broker statistics
		// This would require access to broker internal statistics about
		// active subscribers, consumer groups, etc.
		return nil
	})

	if err != nil {
		// If connection fails, return empty data
		return &SubscribersData{
			Subscribers:       subscribers,
			TotalSubscribers:  len(subscribers),
			ActiveSubscribers: 0,
			LastUpdated:       time.Now(),
		}, nil
	}

	activeCount := 0
	for _, sub := range subscribers {
		if sub.Status == "active" {
			activeCount++
		}
	}

	return &SubscribersData{
		Subscribers:       subscribers,
		TotalSubscribers:  len(subscribers),
		ActiveSubscribers: activeCount,
		LastUpdated:       time.Now(),
	}, nil
}

// GetTopicDetails retrieves detailed information about a specific topic
func (s *AdminServer) GetTopicDetails(namespace, topicName string) (*TopicDetailsData, error) {
	// Find broker leader
	brokerLeader, err := s.findBrokerLeader()
	if err != nil {
		return nil, fmt.Errorf("failed to find broker leader: %w", err)
	}

	var topicDetails *TopicDetailsData

	// Connect to broker leader and get topic configuration
	err = s.withBrokerClient(brokerLeader, func(client mq_pb.SeaweedMessagingClient) error {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		// Get topic configuration using the new API
		configResp, err := client.GetTopicConfiguration(ctx, &mq_pb.GetTopicConfigurationRequest{
			Topic: &schema_pb.Topic{
				Namespace: namespace,
				Name:      topicName,
			},
		})
		if err != nil {
			return fmt.Errorf("failed to get topic configuration: %w", err)
		}

		// Initialize topic details
		topicDetails = &TopicDetailsData{
			TopicName:            fmt.Sprintf("%s.%s", namespace, topicName),
			Namespace:            namespace,
			Name:                 topicName,
			Partitions:           []PartitionInfo{},
			Publishers:           []PublisherInfo{},
			Subscribers:          []TopicSubscriberInfo{},
			ConsumerGroupOffsets: []ConsumerGroupOffsetInfo{},
			Retention:            convertTopicRetention(configResp.Retention),
			CreatedAt:            time.Unix(0, configResp.CreatedAtNs),
			LastUpdated:          time.Unix(0, configResp.LastUpdatedNs),
		}

		// Set current time if timestamps are not available
		if configResp.CreatedAtNs == 0 {
			topicDetails.CreatedAt = time.Now()
		}
		if configResp.LastUpdatedNs == 0 {
			topicDetails.LastUpdated = time.Now()
		}

		// Process partitions
		for _, assignment := range configResp.BrokerPartitionAssignments {
			if assignment.Partition != nil {
				partitionInfo := PartitionInfo{
					ID:             assignment.Partition.RangeStart,
					LeaderBroker:   assignment.LeaderBroker,
					FollowerBroker: assignment.FollowerBroker,
					MessageCount:   0,           // Will be enhanced later with actual stats
					TotalSize:      0,           // Will be enhanced later with actual stats
					LastDataTime:   time.Time{}, // Will be enhanced later
					CreatedAt:      time.Now(),
				}
				topicDetails.Partitions = append(topicDetails.Partitions, partitionInfo)
			}
		}

		// Process flat schema format
		if configResp.MessageRecordType != nil {
			for _, field := range configResp.MessageRecordType.Fields {
				isKey := false
				for _, keyCol := range configResp.KeyColumns {
					if field.Name == keyCol {
						isKey = true
						break
					}
				}

				fieldType := "UNKNOWN"
				if field.Type != nil && field.Type.Kind != nil {
					fieldType = getFieldTypeName(field.Type)
				}

				schemaField := SchemaFieldInfo{
					Name: field.Name,
					Type: fieldType,
				}

				if isKey {
					topicDetails.KeySchema = append(topicDetails.KeySchema, schemaField)
				} else {
					topicDetails.ValueSchema = append(topicDetails.ValueSchema, schemaField)
				}
			}
		}

		// Get publishers information
		publishersResp, err := client.GetTopicPublishers(ctx, &mq_pb.GetTopicPublishersRequest{
			Topic: &schema_pb.Topic{
				Namespace: namespace,
				Name:      topicName,
			},
		})
		if err != nil {
			// Log error but don't fail the entire request
			glog.V(0).Infof("failed to get topic publishers for %s.%s: %v", namespace, topicName, err)
		} else {
			glog.V(1).Infof("got %d publishers for topic %s.%s", len(publishersResp.Publishers), namespace, topicName)
			topicDetails.Publishers = convertTopicPublishers(publishersResp.Publishers)
		}

		// Get subscribers information
		subscribersResp, err := client.GetTopicSubscribers(ctx, &mq_pb.GetTopicSubscribersRequest{
			Topic: &schema_pb.Topic{
				Namespace: namespace,
				Name:      topicName,
			},
		})
		if err != nil {
			// Log error but don't fail the entire request
			glog.V(0).Infof("failed to get topic subscribers for %s.%s: %v", namespace, topicName, err)
		} else {
			glog.V(1).Infof("got %d subscribers for topic %s.%s", len(subscribersResp.Subscribers), namespace, topicName)
			topicDetails.Subscribers = convertTopicSubscribers(subscribersResp.Subscribers)
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	// Get consumer group offsets from the filer
	offsets, err := s.GetConsumerGroupOffsets(namespace, topicName)
	if err != nil {
		// Log error but don't fail the entire request
		glog.V(0).Infof("failed to get consumer group offsets for %s.%s: %v", namespace, topicName, err)
	} else {
		glog.V(1).Infof("got %d consumer group offsets for topic %s.%s", len(offsets), namespace, topicName)
		topicDetails.ConsumerGroupOffsets = offsets
	}

	return topicDetails, nil
}

// GetConsumerGroupOffsets retrieves consumer group offsets for a topic from the filer
func (s *AdminServer) GetConsumerGroupOffsets(namespace, topicName string) ([]ConsumerGroupOffsetInfo, error) {
	var offsets []ConsumerGroupOffsetInfo

	err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
		// Get the topic directory: /topics/namespace/topicName
		topicObj := topic.NewTopic(namespace, topicName)
		topicDir := topicObj.Dir()

		// List all version directories under the topic directory (e.g., v2025-07-10-05-44-34)
		versionStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
			Directory:          topicDir,
			Prefix:             "",
			StartFromFileName:  "",
			InclusiveStartFrom: false,
			Limit:              1000,
		})
		if err != nil {
			return fmt.Errorf("failed to list topic directory %s: %v", topicDir, err)
		}

		// Process each version directory
		for {
			versionResp, err := versionStream.Recv()
			if err != nil {
				if err == io.EOF {
					break
				}
				return fmt.Errorf("failed to receive version entries: %w", err)
			}

			// Only process directories that are versions (start with "v")
			if versionResp.Entry != nil && versionResp.Entry.IsDirectory && strings.HasPrefix(versionResp.Entry.Name, "v") {
				versionDir := path.Join(topicDir, versionResp.Entry.Name)

				// List all partition directories under the version directory (e.g., 0315-0630)
				partitionStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
					Directory:          versionDir,
					Prefix:             "",
					StartFromFileName:  "",
					InclusiveStartFrom: false,
					Limit:              1000,
				})
				if err != nil {
					glog.Warningf("Failed to list version directory %s: %v", versionDir, err)
					continue
				}

				// Process each partition directory
				for {
					partitionResp, err := partitionStream.Recv()
					if err != nil {
						if err == io.EOF {
							break
						}
						glog.Warningf("Failed to receive partition entries: %v", err)
						break
					}

					// Only process directories that are partitions (format: NNNN-NNNN)
					if partitionResp.Entry != nil && partitionResp.Entry.IsDirectory {
						// Parse partition range to get partition start ID (e.g., "0315-0630" -> 315)
						var partitionStart, partitionStop int32
						if n, err := fmt.Sscanf(partitionResp.Entry.Name, "%04d-%04d", &partitionStart, &partitionStop); n != 2 || err != nil {
							// Skip directories that don't match the partition format
							continue
						}

						partitionDir := path.Join(versionDir, partitionResp.Entry.Name)

						// List all .offset files in this partition directory
						offsetStream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{
							Directory:          partitionDir,
							Prefix:             "",
							StartFromFileName:  "",
							InclusiveStartFrom: false,
							Limit:              1000,
						})
						if err != nil {
							glog.Warningf("Failed to list partition directory %s: %v", partitionDir, err)
							continue
						}

						// Process each offset file
						for {
							offsetResp, err := offsetStream.Recv()
							if err != nil {
								if err == io.EOF {
									break
								}
								glog.Warningf("Failed to receive offset entries: %v", err)
								break
							}

							// Only process .offset files
							if offsetResp.Entry != nil && !offsetResp.Entry.IsDirectory && strings.HasSuffix(offsetResp.Entry.Name, ".offset") {
								consumerGroup := strings.TrimSuffix(offsetResp.Entry.Name, ".offset")

								// Read the offset value from the file
								offsetData, err := filer.ReadInsideFiler(context.Background(), client, partitionDir, offsetResp.Entry.Name)
								if err != nil {
									glog.Warningf("Failed to read offset file %s: %v", offsetResp.Entry.Name, err)
									continue
								}

								if len(offsetData) == 8 {
									offset := int64(util.BytesToUint64(offsetData))

									// Get the file modification time
									var lastUpdated time.Time
									if offsetResp.Entry.Attributes != nil {
										lastUpdated = time.Unix(offsetResp.Entry.Attributes.Mtime, 0)
									}

									offsets = append(offsets, ConsumerGroupOffsetInfo{
										ConsumerGroup: consumerGroup,
										PartitionID:   partitionStart, // Use partition start as the ID
										Offset:        offset,
										LastUpdated:   lastUpdated,
									})
								}
							}
						}
					}
				}
			}
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to get consumer group offsets: %w", err)
	}

	return offsets, nil
}

// convertTopicPublishers converts protobuf TopicPublisher slice to PublisherInfo slice
func convertTopicPublishers(publishers []*mq_pb.TopicPublisher) []PublisherInfo {
	publisherInfos := make([]PublisherInfo, 0, len(publishers))

	for _, publisher := range publishers {
		publisherInfo := PublisherInfo{
			PublisherName:       publisher.PublisherName,
			ClientID:            publisher.ClientId,
			PartitionID:         publisher.Partition.RangeStart,
			Broker:              publisher.Broker,
			IsActive:            publisher.IsActive,
			LastPublishedOffset: publisher.LastPublishedOffset,
			LastAckedOffset:     publisher.LastAckedOffset,
		}

		// Convert timestamps
		if publisher.ConnectTimeNs > 0 {
			publisherInfo.ConnectTime = time.Unix(0, publisher.ConnectTimeNs)
		}
		if publisher.LastSeenTimeNs > 0 {
			publisherInfo.LastSeenTime = time.Unix(0, publisher.LastSeenTimeNs)
		}

		publisherInfos = append(publisherInfos, publisherInfo)
	}

	return publisherInfos
}

// convertTopicSubscribers converts protobuf TopicSubscriber slice to TopicSubscriberInfo slice
func convertTopicSubscribers(subscribers []*mq_pb.TopicSubscriber) []TopicSubscriberInfo {
	subscriberInfos := make([]TopicSubscriberInfo, 0, len(subscribers))

	for _, subscriber := range subscribers {
		subscriberInfo := TopicSubscriberInfo{
			ConsumerGroup:      subscriber.ConsumerGroup,
			ConsumerID:         subscriber.ConsumerId,
			ClientID:           subscriber.ClientId,
			PartitionID:        subscriber.Partition.RangeStart,
			Broker:             subscriber.Broker,
			IsActive:           subscriber.IsActive,
			CurrentOffset:      subscriber.CurrentOffset,
			LastReceivedOffset: subscriber.LastReceivedOffset,
		}

		// Convert timestamps
		if subscriber.ConnectTimeNs > 0 {
			subscriberInfo.ConnectTime = time.Unix(0, subscriber.ConnectTimeNs)
		}
		if subscriber.LastSeenTimeNs > 0 {
			subscriberInfo.LastSeenTime = time.Unix(0, subscriber.LastSeenTimeNs)
		}

		subscriberInfos = append(subscriberInfos, subscriberInfo)
	}

	return subscriberInfos
}

// findBrokerLeader finds the current broker leader
func (s *AdminServer) findBrokerLeader() (string, error) {
	// First, try to find any broker from the cluster
	var brokers []string
	err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
		resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
			ClientType: cluster.BrokerType,
		})
		if err != nil {
			return err
		}

		for _, node := range resp.ClusterNodes {
			brokers = append(brokers, node.Address)
		}

		return nil
	})

	if err != nil {
		return "", fmt.Errorf("failed to list brokers: %w", err)
	}

	if len(brokers) == 0 {
		return "", fmt.Errorf("no brokers found in cluster")
	}

	// Try each broker to find the leader
	for _, brokerAddr := range brokers {
		err := s.withBrokerClient(brokerAddr, func(client mq_pb.SeaweedMessagingClient) error {
			ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
			defer cancel()

			// Try to find broker leader
			_, err := client.FindBrokerLeader(ctx, &mq_pb.FindBrokerLeaderRequest{
				FilerGroup: "",
			})
			if err == nil {
				return nil // This broker is the leader
			}
			return err
		})
		if err == nil {
			return brokerAddr, nil
		}
	}

	return "", fmt.Errorf("no broker leader found")
}

// withBrokerClient connects to a message queue broker and executes a function
func (s *AdminServer) withBrokerClient(brokerAddress string, fn func(client mq_pb.SeaweedMessagingClient) error) error {
	return pb.WithBrokerGrpcClient(false, brokerAddress, s.grpcDialOption, fn)
}

// convertTopicRetention converts protobuf retention to TopicRetentionInfo
func convertTopicRetention(retention *mq_pb.TopicRetention) TopicRetentionInfo {
	if retention == nil || !retention.Enabled {
		return TopicRetentionInfo{
			Enabled:          false,
			RetentionSeconds: 0,
			DisplayValue:     0,
			DisplayUnit:      "days",
		}
	}

	// Convert seconds to human-readable format
	seconds := retention.RetentionSeconds
	var displayValue int32
	var displayUnit string

	if seconds >= 86400 { // >= 1 day
		displayValue = int32(seconds / 86400)
		displayUnit = "days"
	} else if seconds >= 3600 { // >= 1 hour
		displayValue = int32(seconds / 3600)
		displayUnit = "hours"
	} else {
		displayValue = int32(seconds)
		displayUnit = "seconds"
	}

	return TopicRetentionInfo{
		Enabled:          retention.Enabled,
		RetentionSeconds: seconds,
		DisplayValue:     displayValue,
		DisplayUnit:      displayUnit,
	}
}

// getFieldTypeName converts a schema_pb.Type to a human-readable type name
func getFieldTypeName(fieldType *schema_pb.Type) string {
	if fieldType.Kind == nil {
		return "UNKNOWN"
	}

	switch kind := fieldType.Kind.(type) {
	case *schema_pb.Type_ScalarType:
		switch kind.ScalarType {
		case schema_pb.ScalarType_BOOL:
			return "BOOLEAN"
		case schema_pb.ScalarType_INT32:
			return "INT32"
		case schema_pb.ScalarType_INT64:
			return "INT64"
		case schema_pb.ScalarType_FLOAT:
			return "FLOAT"
		case schema_pb.ScalarType_DOUBLE:
			return "DOUBLE"
		case schema_pb.ScalarType_BYTES:
			return "BYTES"
		case schema_pb.ScalarType_STRING:
			return "STRING"
		case schema_pb.ScalarType_TIMESTAMP:
			return "TIMESTAMP"
		case schema_pb.ScalarType_DATE:
			return "DATE"
		case schema_pb.ScalarType_TIME:
			return "TIME"
		case schema_pb.ScalarType_DECIMAL:
			return "DECIMAL"
		default:
			return "SCALAR"
		}
	case *schema_pb.Type_ListType:
		return "LIST"
	case *schema_pb.Type_RecordType:
		return "RECORD"
	default:
		return "UNKNOWN"
	}
}
