package main

import (
	"crypto/subtle"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const VERSION = "2.3.0"

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

type DeletedItem struct {
	ID        string `json:"id"`
	Type      string `json:"type"`
	DeletedAt string `json:"deletedAt"`
}

type ClimbDataBackup struct {
	ExportedAt    string               `json:"exportedAt"`
	Version       string               `json:"version"`
	FormatVersion string               `json:"formatVersion"`
	Gyms          []BackupGym          `json:"gyms"`
	Problems      []BackupProblem      `json:"problems"`
	Sessions      []BackupClimbSession `json:"sessions"`
	Attempts      []BackupAttempt      `json:"attempts"`
	DeletedItems  []DeletedItem        `json:"deletedItems"`
}

type DeltaSyncRequest struct {
	LastSyncTime string               `json:"lastSyncTime"`
	Gyms         []BackupGym          `json:"gyms"`
	Problems     []BackupProblem      `json:"problems"`
	Sessions     []BackupClimbSession `json:"sessions"`
	Attempts     []BackupAttempt      `json:"attempts"`
	DeletedItems []DeletedItem        `json:"deletedItems"`
}

type DeltaSyncResponse struct {
	ServerTime   string               `json:"serverTime"`
	Gyms         []BackupGym          `json:"gyms"`
	Problems     []BackupProblem      `json:"problems"`
	Sessions     []BackupClimbSession `json:"sessions"`
	Attempts     []BackupAttempt      `json:"attempts"`
	DeletedItems []DeletedItem        `json:"deletedItems"`
}

type BackupGym struct {
	ID                     string   `json:"id"`
	Name                   string   `json:"name"`
	Location               *string  `json:"location,omitempty"`
	SupportedClimbTypes    []string `json:"supportedClimbTypes"`
	DifficultySystems      []string `json:"difficultySystems"`
	CustomDifficultyGrades []string `json:"customDifficultyGrades"`
	Notes                  *string  `json:"notes,omitempty"`
	CreatedAt              string   `json:"createdAt"`
	UpdatedAt              string   `json:"updatedAt"`
}

type BackupProblem struct {
	ID          string          `json:"id"`
	GymID       string          `json:"gymId"`
	Name        *string         `json:"name,omitempty"`
	Description *string         `json:"description,omitempty"`
	ClimbType   string          `json:"climbType"`
	Difficulty  DifficultyGrade `json:"difficulty"`
	Tags        []string        `json:"tags"`
	Location    *string         `json:"location,omitempty"`
	ImagePaths  []string        `json:"imagePaths,omitempty"`
	IsActive    bool            `json:"isActive"`
	DateSet     *string         `json:"dateSet,omitempty"`
	Notes       *string         `json:"notes,omitempty"`
	CreatedAt   string          `json:"createdAt"`
	UpdatedAt   string          `json:"updatedAt"`
}

type DifficultyGrade struct {
	System       string `json:"system"`
	Grade        string `json:"grade"`
	NumericValue int    `json:"numericValue"`
}

type BackupClimbSession struct {
	ID        string  `json:"id"`
	GymID     string  `json:"gymId"`
	Date      string  `json:"date"`
	StartTime *string `json:"startTime,omitempty"`
	EndTime   *string `json:"endTime,omitempty"`
	Duration  *int64  `json:"duration,omitempty"`
	Status    string  `json:"status"`
	Notes     *string `json:"notes,omitempty"`
	CreatedAt string  `json:"createdAt"`
	UpdatedAt string  `json:"updatedAt"`
}

type BackupAttempt struct {
	ID          string  `json:"id"`
	SessionID   string  `json:"sessionId"`
	ProblemID   string  `json:"problemId"`
	Result      string  `json:"result"`
	HighestHold *string `json:"highestHold,omitempty"`
	Notes       *string `json:"notes,omitempty"`
	Duration    *int64  `json:"duration,omitempty"`
	RestTime    *int64  `json:"restTime,omitempty"`
	Timestamp   string  `json:"timestamp"`
	CreatedAt   string  `json:"createdAt"`
}

type SyncServer struct {
	authToken string
	dataFile  string
	imagesDir string
}

func (s *SyncServer) authenticate(r *http.Request) bool {
	authHeader := r.Header.Get("Authorization")
	if !strings.HasPrefix(authHeader, "Bearer ") {
		return false
	}

	token := strings.TrimPrefix(authHeader, "Bearer ")
	return subtle.ConstantTimeCompare([]byte(token), []byte(s.authToken)) == 1
}

func (s *SyncServer) loadData() (*ClimbDataBackup, error) {
	log.Printf("Loading data from: %s", s.dataFile)

	if _, err := os.Stat(s.dataFile); os.IsNotExist(err) {
		log.Printf("Data file does not exist, creating empty backup")
		return &ClimbDataBackup{
			ExportedAt:    time.Now().UTC().Format(time.RFC3339),
			Version:       "2.0",
			FormatVersion: "2.0",
			Gyms:          []BackupGym{},
			Problems:      []BackupProblem{},
			Sessions:      []BackupClimbSession{},
			Attempts:      []BackupAttempt{},
			DeletedItems:  []DeletedItem{},
		}, nil
	}

	data, err := os.ReadFile(s.dataFile)
	if err != nil {
		log.Printf("Failed to read data file: %v", err)
		return nil, err
	}

	log.Printf("Read %d bytes from data file", len(data))
	log.Printf("File content preview: %s", string(data[:min(200, len(data))]))

	var backup ClimbDataBackup
	if err := json.Unmarshal(data, &backup); err != nil {
		log.Printf("Failed to unmarshal JSON: %v", err)
		return nil, err
	}

	log.Printf("Loaded backup: gyms=%d, problems=%d, sessions=%d, attempts=%d",
		len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))

	return &backup, nil
}

func (s *SyncServer) mergeGyms(existing []BackupGym, updates []BackupGym) []BackupGym {
	gymMap := make(map[string]BackupGym)
	for _, gym := range existing {
		gymMap[gym.ID] = gym
	}

	for _, gym := range updates {
		if existingGym, exists := gymMap[gym.ID]; exists {
			// Keep newer version based on updatedAt timestamp
			if gym.UpdatedAt >= existingGym.UpdatedAt {
				gymMap[gym.ID] = gym
			}
		} else {
			gymMap[gym.ID] = gym
		}
	}

	result := make([]BackupGym, 0, len(gymMap))
	for _, gym := range gymMap {
		result = append(result, gym)
	}
	return result
}

func (s *SyncServer) mergeProblems(existing []BackupProblem, updates []BackupProblem) []BackupProblem {
	problemMap := make(map[string]BackupProblem)
	for _, problem := range existing {
		problemMap[problem.ID] = problem
	}

	for _, problem := range updates {
		if existingProblem, exists := problemMap[problem.ID]; exists {
			if problem.UpdatedAt >= existingProblem.UpdatedAt {
				problemMap[problem.ID] = problem
			}
		} else {
			problemMap[problem.ID] = problem
		}
	}

	result := make([]BackupProblem, 0, len(problemMap))
	for _, problem := range problemMap {
		result = append(result, problem)
	}
	return result
}

func (s *SyncServer) mergeSessions(existing []BackupClimbSession, updates []BackupClimbSession) []BackupClimbSession {
	sessionMap := make(map[string]BackupClimbSession)
	for _, session := range existing {
		sessionMap[session.ID] = session
	}

	for _, session := range updates {
		if existingSession, exists := sessionMap[session.ID]; exists {
			if session.UpdatedAt >= existingSession.UpdatedAt {
				sessionMap[session.ID] = session
			}
		} else {
			sessionMap[session.ID] = session
		}
	}

	result := make([]BackupClimbSession, 0, len(sessionMap))
	for _, session := range sessionMap {
		result = append(result, session)
	}
	return result
}

func (s *SyncServer) mergeAttempts(existing []BackupAttempt, updates []BackupAttempt) []BackupAttempt {
	attemptMap := make(map[string]BackupAttempt)
	for _, attempt := range existing {
		attemptMap[attempt.ID] = attempt
	}

	for _, attempt := range updates {
		if existingAttempt, exists := attemptMap[attempt.ID]; exists {
			if attempt.CreatedAt >= existingAttempt.CreatedAt {
				attemptMap[attempt.ID] = attempt
			}
		} else {
			attemptMap[attempt.ID] = attempt
		}
	}

	result := make([]BackupAttempt, 0, len(attemptMap))
	for _, attempt := range attemptMap {
		result = append(result, attempt)
	}
	return result
}

func (s *SyncServer) mergeDeletedItems(existing []DeletedItem, updates []DeletedItem) []DeletedItem {
	deletedMap := make(map[string]DeletedItem)
	for _, item := range existing {
		key := item.Type + ":" + item.ID
		deletedMap[key] = item
	}

	for _, item := range updates {
		key := item.Type + ":" + item.ID
		if existingItem, exists := deletedMap[key]; exists {
			if item.DeletedAt >= existingItem.DeletedAt {
				deletedMap[key] = item
			}
		} else {
			deletedMap[key] = item
		}
	}

	// Clean up tombstones older than 30 days to prevent unbounded growth
	cutoffTime := time.Now().UTC().Add(-30 * 24 * time.Hour)
	result := make([]DeletedItem, 0, len(deletedMap))
	for _, item := range deletedMap {
		deletedTime, err := time.Parse(time.RFC3339, item.DeletedAt)
		if err == nil && deletedTime.Before(cutoffTime) {
			log.Printf("Cleaning up old deletion record: type=%s, id=%s, deletedAt=%s",
				item.Type, item.ID, item.DeletedAt)
			continue
		}
		result = append(result, item)
	}
	return result
}

func (s *SyncServer) applyDeletions(backup *ClimbDataBackup, deletedItems []DeletedItem) {
	deletedMap := make(map[string]map[string]bool)
	for _, item := range deletedItems {
		if deletedMap[item.Type] == nil {
			deletedMap[item.Type] = make(map[string]bool)
		}
		deletedMap[item.Type][item.ID] = true
	}

	if deletedMap["gym"] != nil {
		filtered := []BackupGym{}
		for _, gym := range backup.Gyms {
			if !deletedMap["gym"][gym.ID] {
				filtered = append(filtered, gym)
			}
		}
		backup.Gyms = filtered
	}

	if deletedMap["problem"] != nil {
		filtered := []BackupProblem{}
		for _, problem := range backup.Problems {
			if !deletedMap["problem"][problem.ID] {
				filtered = append(filtered, problem)
			}
		}
		backup.Problems = filtered
	}

	if deletedMap["session"] != nil {
		filtered := []BackupClimbSession{}
		for _, session := range backup.Sessions {
			if !deletedMap["session"][session.ID] {
				filtered = append(filtered, session)
			}
		}
		backup.Sessions = filtered
	}

	if deletedMap["attempt"] != nil {
		filtered := []BackupAttempt{}
		for _, attempt := range backup.Attempts {
			if !deletedMap["attempt"][attempt.ID] {
				filtered = append(filtered, attempt)
			}
		}
		backup.Attempts = filtered
	}
}

func (s *SyncServer) saveData(backup *ClimbDataBackup) error {
	backup.ExportedAt = time.Now().UTC().Format(time.RFC3339)

	data, err := json.MarshalIndent(backup, "", "  ")
	if err != nil {
		return err
	}

	dir := filepath.Dir(s.dataFile)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	if err := os.MkdirAll(s.imagesDir, 0755); err != nil {
		return err
	}

	return os.WriteFile(s.dataFile, data, 0644)
}

func (s *SyncServer) handleGet(w http.ResponseWriter, r *http.Request) {
	if !s.authenticate(r) {
		log.Printf("Unauthorized access attempt from %s", r.RemoteAddr)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	log.Printf("GET /sync request from %s", r.RemoteAddr)
	backup, err := s.loadData()
	if err != nil {
		log.Printf("Failed to load data: %v", err)
		http.Error(w, "Failed to load data", http.StatusInternalServerError)
		return
	}

	log.Printf("Sending data to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d",
		r.RemoteAddr, len(backup.Gyms), len(backup.Problems), len(backup.Sessions), len(backup.Attempts))
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(backup)
}

func (s *SyncServer) handlePut(w http.ResponseWriter, r *http.Request) {
	if !s.authenticate(r) {
		log.Printf("Unauthorized sync attempt from %s", r.RemoteAddr)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	var backup ClimbDataBackup
	if err := json.NewDecoder(r.Body).Decode(&backup); err != nil {
		log.Printf("Invalid JSON from %s: %v", r.RemoteAddr, err)
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	if err := s.saveData(&backup); err != nil {
		log.Printf("Failed to save data: %v", err)
		http.Error(w, "Failed to save data", http.StatusInternalServerError)
		return
	}

	log.Printf("Data synced by %s", r.RemoteAddr)
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(backup)
}

func (s *SyncServer) handleHealth(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]string{
		"status":  "healthy",
		"version": VERSION,
		"time":    time.Now().UTC().Format(time.RFC3339),
	})
}

func (s *SyncServer) handleImageUpload(w http.ResponseWriter, r *http.Request) {
	if !s.authenticate(r) {
		log.Printf("Unauthorized image upload attempt from %s", r.RemoteAddr)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	filename := r.URL.Query().Get("filename")
	if filename == "" {
		http.Error(w, "Missing filename parameter", http.StatusBadRequest)
		return
	}

	imageData, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read image data", http.StatusBadRequest)
		return
	}

	imagePath := filepath.Join(s.imagesDir, filename)
	if err := os.WriteFile(imagePath, imageData, 0644); err != nil {
		log.Printf("Failed to save image %s: %v", filename, err)
		http.Error(w, "Failed to save image", http.StatusInternalServerError)
		return
	}

	log.Printf("Image uploaded: %s (%d bytes) by %s", filename, len(imageData), r.RemoteAddr)
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]string{"status": "uploaded"})
}

func (s *SyncServer) handleImageDownload(w http.ResponseWriter, r *http.Request) {
	if !s.authenticate(r) {
		log.Printf("Unauthorized image download attempt from %s", r.RemoteAddr)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	filename := r.URL.Query().Get("filename")
	if filename == "" {
		http.Error(w, "Missing filename parameter", http.StatusBadRequest)
		return
	}

	imagePath := filepath.Join(s.imagesDir, filename)
	imageData, err := os.ReadFile(imagePath)
	if err != nil {
		if os.IsNotExist(err) {
			http.Error(w, "Image not found", http.StatusNotFound)
		} else {
			http.Error(w, "Failed to read image", http.StatusInternalServerError)
		}
		return
	}

	// Set appropriate content type based on file extension
	ext := filepath.Ext(filename)
	switch ext {
	case ".jpg", ".jpeg":
		w.Header().Set("Content-Type", "image/jpeg")
	case ".png":
		w.Header().Set("Content-Type", "image/png")
	case ".gif":
		w.Header().Set("Content-Type", "image/gif")
	case ".webp":
		w.Header().Set("Content-Type", "image/webp")
	default:
		w.Header().Set("Content-Type", "application/octet-stream")
	}

	w.WriteHeader(http.StatusOK)
	w.Write(imageData)
}

func (s *SyncServer) handleDeltaSync(w http.ResponseWriter, r *http.Request) {
	if !s.authenticate(r) {
		log.Printf("Unauthorized delta sync attempt from %s", r.RemoteAddr)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var deltaRequest DeltaSyncRequest
	if err := json.NewDecoder(r.Body).Decode(&deltaRequest); err != nil {
		log.Printf("Invalid JSON from %s: %v", r.RemoteAddr, err)
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	log.Printf("Delta sync from %s: lastSyncTime=%s, gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
		r.RemoteAddr, deltaRequest.LastSyncTime,
		len(deltaRequest.Gyms), len(deltaRequest.Problems),
		len(deltaRequest.Sessions), len(deltaRequest.Attempts),
		len(deltaRequest.DeletedItems))

	// Load current server data
	serverBackup, err := s.loadData()
	if err != nil {
		log.Printf("Failed to load data: %v", err)
		http.Error(w, "Failed to load data", http.StatusInternalServerError)
		return
	}

	// Merge and apply deletions first to prevent resurrection
	serverBackup.DeletedItems = s.mergeDeletedItems(serverBackup.DeletedItems, deltaRequest.DeletedItems)
	s.applyDeletions(serverBackup, serverBackup.DeletedItems)
	log.Printf("Applied deletions: total=%d deletion records", len(serverBackup.DeletedItems))

	// Merge client changes into server data
	serverBackup.Gyms = s.mergeGyms(serverBackup.Gyms, deltaRequest.Gyms)
	serverBackup.Problems = s.mergeProblems(serverBackup.Problems, deltaRequest.Problems)
	serverBackup.Sessions = s.mergeSessions(serverBackup.Sessions, deltaRequest.Sessions)
	serverBackup.Attempts = s.mergeAttempts(serverBackup.Attempts, deltaRequest.Attempts)

	// Save merged data
	if err := s.saveData(serverBackup); err != nil {
		log.Printf("Failed to save data: %v", err)
		http.Error(w, "Failed to save data", http.StatusInternalServerError)
		return
	}

	// Parse client's last sync time
	clientLastSync, err := time.Parse(time.RFC3339, deltaRequest.LastSyncTime)
	if err != nil {
		clientLastSync = time.Time{}
		log.Printf("Warning: Could not parse lastSyncTime '%s', sending all data", deltaRequest.LastSyncTime)
	}

	// Build deleted item lookup map
	deletedItemMap := make(map[string]bool)
	for _, item := range serverBackup.DeletedItems {
		key := item.Type + ":" + item.ID
		deletedItemMap[key] = true
	}

	// Prepare response with items modified since client's last sync
	response := DeltaSyncResponse{
		ServerTime:   time.Now().UTC().Format(time.RFC3339),
		Gyms:         []BackupGym{},
		Problems:     []BackupProblem{},
		Sessions:     []BackupClimbSession{},
		Attempts:     []BackupAttempt{},
		DeletedItems: []DeletedItem{},
	}

	// Filter gyms modified after client's last sync
	for _, gym := range serverBackup.Gyms {
		if deletedItemMap["gym:"+gym.ID] {
			continue
		}
		gymTime, err := time.Parse(time.RFC3339, gym.UpdatedAt)
		if err == nil && gymTime.After(clientLastSync) {
			response.Gyms = append(response.Gyms, gym)
		}
	}

	// Filter problems modified after client's last sync
	for _, problem := range serverBackup.Problems {
		if deletedItemMap["problem:"+problem.ID] {
			continue
		}
		problemTime, err := time.Parse(time.RFC3339, problem.UpdatedAt)
		if err == nil && problemTime.After(clientLastSync) {
			response.Problems = append(response.Problems, problem)
		}
	}

	// Filter sessions modified after client's last sync
	for _, session := range serverBackup.Sessions {
		if deletedItemMap["session:"+session.ID] {
			continue
		}
		sessionTime, err := time.Parse(time.RFC3339, session.UpdatedAt)
		if err == nil && sessionTime.After(clientLastSync) {
			response.Sessions = append(response.Sessions, session)
		}
	}

	// Filter attempts created after client's last sync
	for _, attempt := range serverBackup.Attempts {
		if deletedItemMap["attempt:"+attempt.ID] {
			continue
		}
		attemptTime, err := time.Parse(time.RFC3339, attempt.CreatedAt)
		if err == nil && attemptTime.After(clientLastSync) {
			response.Attempts = append(response.Attempts, attempt)
		}
	}

	// Filter deletions after client's last sync
	for _, deletedItem := range serverBackup.DeletedItems {
		deletedTime, err := time.Parse(time.RFC3339, deletedItem.DeletedAt)
		if err == nil && deletedTime.After(clientLastSync) {
			response.DeletedItems = append(response.DeletedItems, deletedItem)
		}
	}

	log.Printf("Delta sync response to %s: gyms=%d, problems=%d, sessions=%d, attempts=%d, deletedItems=%d",
		r.RemoteAddr,
		len(response.Gyms), len(response.Problems),
		len(response.Sessions), len(response.Attempts),
		len(response.DeletedItems))

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

func (s *SyncServer) handleSync(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		s.handleGet(w, r)
	case http.MethodPut:
		s.handlePut(w, r)
	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func main() {
	authToken := os.Getenv("AUTH_TOKEN")
	if authToken == "" {
		log.Fatal("AUTH_TOKEN environment variable is required")
	}

	dataFile := os.Getenv("DATA_FILE")
	if dataFile == "" {
		dataFile = "./data/climb_data.json"
	}

	imagesDir := os.Getenv("IMAGES_DIR")
	if imagesDir == "" {
		imagesDir = "./data/images"
	}

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	server := &SyncServer{
		authToken: authToken,
		dataFile:  dataFile,
		imagesDir: imagesDir,
	}

	http.HandleFunc("/sync", server.handleSync)
	http.HandleFunc("/sync/delta", server.handleDeltaSync)
	http.HandleFunc("/health", server.handleHealth)
	http.HandleFunc("/images/upload", server.handleImageUpload)
	http.HandleFunc("/images/download", server.handleImageDownload)

	fmt.Printf("Ascently sync server v%s starting on port %s\n", VERSION, port)
	fmt.Printf("Data file: %s\n", dataFile)
	fmt.Printf("Images directory: %s\n", imagesDir)
	fmt.Printf("Health check available at /health\n")
	fmt.Printf("Delta sync: POST /sync/delta (incremental sync)\n")
	fmt.Printf("Full sync: GET /sync (download all), PUT /sync (upload all)\n")
	fmt.Printf("Image upload: POST /images/upload?filename=<name>\n")
	fmt.Printf("Image download: GET /images/download?filename=<name>\n")

	log.Fatal(http.ListenAndServe(":"+port, nil))
}
