package config

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"github.com/rs/zerolog/log"
)

// DockerMetadata holds additional metadata for a Docker resource (container/service)
type DockerMetadata struct {
	ID          string   `json:"id"`          // Resource ID (e.g., "hostid:container:containerid" or "hostid:service:serviceid")
	CustomURL   string   `json:"customUrl"`   // Custom URL for the resource
	Description string   `json:"description"` // Optional description
	Tags        []string `json:"tags"`        // Optional tags for categorization
}

// DockerHostMetadata holds additional metadata for a Docker host
type DockerHostMetadata struct {
	CustomDisplayName string `json:"customDisplayName,omitempty"` // User-defined custom display name
}

// dockerMetadataFile represents the on-disk format for Docker metadata
type dockerMetadataFile struct {
	Containers map[string]*DockerMetadata     `json:"containers,omitempty"` // Container/service metadata (legacy: may be top-level)
	Hosts      map[string]*DockerHostMetadata `json:"hosts,omitempty"`      // Host-level metadata
}

// DockerMetadataStore manages Docker resource metadata
type DockerMetadataStore struct {
	mu           sync.RWMutex
	metadata     map[string]*DockerMetadata     // keyed by resource ID (containers/services)
	hostMetadata map[string]*DockerHostMetadata // keyed by host ID
	dataPath     string
}

// NewDockerMetadataStore creates a new metadata store
func NewDockerMetadataStore(dataPath string) *DockerMetadataStore {
	store := &DockerMetadataStore{
		metadata:     make(map[string]*DockerMetadata),
		hostMetadata: make(map[string]*DockerHostMetadata),
		dataPath:     dataPath,
	}

	// Load existing metadata
	if err := store.Load(); err != nil {
		log.Warn().Err(err).Msg("Failed to load Docker metadata")
	}

	return store
}

// Get retrieves metadata for a Docker resource
func (s *DockerMetadataStore) Get(resourceID string) *DockerMetadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if meta, exists := s.metadata[resourceID]; exists {
		return meta
	}
	return nil
}

// GetAll retrieves all Docker resource metadata
func (s *DockerMetadataStore) GetAll() map[string]*DockerMetadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// Return a copy to prevent external modifications
	result := make(map[string]*DockerMetadata)
	for k, v := range s.metadata {
		result[k] = v
	}
	return result
}

// GetHostMetadata retrieves metadata for a Docker host
func (s *DockerMetadataStore) GetHostMetadata(hostID string) *DockerHostMetadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if meta, exists := s.hostMetadata[hostID]; exists {
		return meta
	}
	return nil
}

// GetAllHostMetadata retrieves all Docker host metadata
func (s *DockerMetadataStore) GetAllHostMetadata() map[string]*DockerHostMetadata {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// Return a copy to prevent external modifications
	result := make(map[string]*DockerHostMetadata)
	for k, v := range s.hostMetadata {
		result[k] = v
	}
	return result
}

// SetHostMetadata updates or creates metadata for a Docker host
func (s *DockerMetadataStore) SetHostMetadata(hostID string, meta *DockerHostMetadata) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if meta == nil || meta.CustomDisplayName == "" {
		// If metadata is nil or custom display name is empty, delete the entry
		delete(s.hostMetadata, hostID)
	} else {
		s.hostMetadata[hostID] = meta
	}

	// Save to disk
	return s.save()
}

// Set updates or creates metadata for a Docker resource
func (s *DockerMetadataStore) Set(resourceID string, meta *DockerMetadata) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if meta == nil {
		return fmt.Errorf("metadata cannot be nil")
	}

	meta.ID = resourceID
	s.metadata[resourceID] = meta

	// Save to disk
	return s.save()
}

// Delete removes metadata for a Docker resource
func (s *DockerMetadataStore) Delete(resourceID string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	delete(s.metadata, resourceID)

	// Save to disk
	return s.save()
}

// ReplaceAll replaces all metadata entries and persists them to disk.
func (s *DockerMetadataStore) ReplaceAll(metadata map[string]*DockerMetadata) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.metadata = make(map[string]*DockerMetadata)

	if metadata != nil {
		for resourceID, meta := range metadata {
			if meta == nil {
				continue
			}

			clone := *meta
			clone.ID = resourceID
			// Ensure slice copy is not nil to allow JSON marshalling of empty tags
			if clone.Tags == nil {
				clone.Tags = []string{}
			}
			s.metadata[resourceID] = &clone
		}
	}

	return s.save()
}

// Load reads metadata from disk
func (s *DockerMetadataStore) Load() error {
	filePath := filepath.Join(s.dataPath, "docker_metadata.json")

	log.Debug().Str("path", filePath).Msg("Loading Docker metadata from disk")

	data, err := os.ReadFile(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			// File doesn't exist yet, not an error
			log.Debug().Str("path", filePath).Msg("Docker metadata file does not exist yet")
			return nil
		}
		return fmt.Errorf("failed to read metadata file: %w", err)
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	// Try to load as versioned format first
	var fileData dockerMetadataFile
	if err := json.Unmarshal(data, &fileData); err != nil {
		return fmt.Errorf("failed to unmarshal metadata: %w", err)
	}

	// Check if this is the new format (has "hosts" or "containers" keys)
	if fileData.Hosts != nil || fileData.Containers != nil {
		// New versioned format
		if fileData.Containers != nil {
			s.metadata = fileData.Containers
		} else {
			s.metadata = make(map[string]*DockerMetadata)
		}
		if fileData.Hosts != nil {
			s.hostMetadata = fileData.Hosts
		} else {
			s.hostMetadata = make(map[string]*DockerHostMetadata)
		}
		log.Info().
			Int("containerCount", len(s.metadata)).
			Int("hostCount", len(s.hostMetadata)).
			Msg("Loaded Docker metadata (versioned format)")
	} else {
		// Legacy format: top-level map is container metadata
		if err := json.Unmarshal(data, &s.metadata); err != nil {
			return fmt.Errorf("failed to unmarshal legacy metadata: %w", err)
		}
		s.hostMetadata = make(map[string]*DockerHostMetadata)
		log.Info().
			Int("containerCount", len(s.metadata)).
			Msg("Loaded Docker metadata (legacy format)")
	}

	return nil
}

// save writes metadata to disk (must be called with lock held)
func (s *DockerMetadataStore) save() error {
	filePath := filepath.Join(s.dataPath, "docker_metadata.json")

	log.Debug().Str("path", filePath).Msg("Saving Docker metadata to disk")

	// Use versioned format
	fileData := dockerMetadataFile{
		Containers: s.metadata,
		Hosts:      s.hostMetadata,
	}

	data, err := json.Marshal(fileData)
	if err != nil {
		return fmt.Errorf("failed to marshal metadata: %w", err)
	}

	// Ensure directory exists
	if err := os.MkdirAll(s.dataPath, 0755); err != nil {
		return fmt.Errorf("failed to create data directory: %w", err)
	}

	// Write to temp file first for atomic operation
	tempFile := filePath + ".tmp"
	if err := os.WriteFile(tempFile, data, 0644); err != nil {
		return fmt.Errorf("failed to write metadata file: %w", err)
	}

	// Rename temp file to actual file (atomic on most systems)
	if err := os.Rename(tempFile, filePath); err != nil {
		return fmt.Errorf("failed to rename metadata file: %w", err)
	}

	log.Debug().Str("path", filePath).Int("containers", len(s.metadata)).Int("hosts", len(s.hostMetadata)).Msg("Docker metadata saved successfully")

	return nil
}
