package sync

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/tim/cairnquire/apps/server/internal/docs"
	"github.com/tim/cairnquire/apps/server/internal/store"
)

// Service handles sync protocol business logic.
type Service struct {
	repo       *Repository
	docService *docs.Service
	contentStore *store.ContentStore
	logger     *slog.Logger
	sourceDir  string
}

// NewService creates a new sync service.
func NewService(repo *Repository, docService *docs.Service, contentStore *store.ContentStore, sourceDir string, logger *slog.Logger) *Service {
	return &Service{
		repo:         repo,
		docService:   docService,
		contentStore: contentStore,
		logger:       logger,
		sourceDir:    sourceDir,
	}
}

// InitSync creates a new snapshot from the current server state.
func (s *Service) InitSync(ctx context.Context, deviceID, userID string) (*Snapshot, error) {
	files, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build snapshot: %w", err)
	}

	snap, err := s.repo.CreateSnapshot(ctx, deviceID, userID, files)
	if err != nil {
		return nil, fmt.Errorf("create snapshot: %w", err)
	}

	s.logger.Info("sync initialized", "device", deviceID, "snapshot", snap.ID, "files", len(files))
	return snap, nil
}

// ApplyDelta processes client changes and computes server delta + conflicts.
func (s *Service) ApplyDelta(ctx context.Context, snapshotID string, clientDelta Delta) (*DeltaResult, error) {
	snap, err := s.repo.GetSnapshot(ctx, snapshotID)
	if err != nil {
		return nil, fmt.Errorf("get snapshot: %w", err)
	}

	serverFiles := make(map[string]FileEntry)
	for _, f := range snap.Files {
		serverFiles[f.Path] = f
	}

	clientFiles := make(map[string]FileEntry)
	for _, c := range clientDelta.Changes {
		if c.Type == ChangeDelete || c.Type == ChangeRename {
			continue
		}
		clientFiles[c.Path] = FileEntry{
			Path:     c.Path,
			Hash:     c.Hash,
			Size:     c.Size,
			Modified: c.Modified,
		}
	}

	// Rebuild current server state (may have changed since snapshot)
	currentFiles, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build current snapshot: %w", err)
	}

	currentMap := make(map[string]FileEntry)
	for _, f := range currentFiles {
		currentMap[f.Path] = f
	}

	var serverDelta []Change
	var conflicts []Conflict

	// Detect server changes since snapshot
	for path, current := range currentMap {
		old, existed := serverFiles[path]
		if !existed {
			// Server created this file
			serverDelta = append(serverDelta, Change{
				Type:     ChangeCreate,
				Path:     path,
				Hash:     current.Hash,
				Size:     current.Size,
				Modified: current.Modified,
			})
		} else if old.Hash != current.Hash {
			// Server updated this file
			serverDelta = append(serverDelta, Change{
				Type:     ChangeUpdate,
				Path:     path,
				Hash:     current.Hash,
				Size:     current.Size,
				Modified: current.Modified,
			})
		}
	}

	// Detect server deletions
	for path := range serverFiles {
		if _, exists := currentMap[path]; !exists {
			serverDelta = append(serverDelta, Change{
				Type: ChangeDelete,
				Path: path,
			})
		}
	}

	// Check for conflicts: both client and server changed same file
	for _, clientChange := range clientDelta.Changes {
		if clientChange.Type == ChangeCreate || clientChange.Type == ChangeUpdate {
			serverCurrent, serverHas := currentMap[clientChange.Path]
			serverOld, serverHad := serverFiles[clientChange.Path]

			if serverHad && serverHas && serverOld.Hash != serverCurrent.Hash &&
				serverCurrent.Hash != clientChange.Hash {
				conflicts = append(conflicts, Conflict{
					Path:           clientChange.Path,
					ServerHash:     serverCurrent.Hash,
					ClientHash:     clientChange.Hash,
					ServerModified: serverCurrent.Modified,
					ClientModified: clientChange.Modified,
					Strategy:       ResolutionLastWriteWins,
				})
			}
		}
	}

	return &DeltaResult{
		ServerDelta: serverDelta,
		Conflicts:   conflicts,
	}, nil
}

// ResolveConflicts applies resolved changes and creates a new snapshot.
func (s *Service) ResolveConflicts(ctx context.Context, snapshotID string, resolutions []Resolution) (*Snapshot, error) {
	snap, err := s.repo.GetSnapshot(ctx, snapshotID)
	if err != nil {
		return nil, fmt.Errorf("get snapshot: %w", err)
	}

	// Build a map of resolutions by path
	resMap := make(map[string]Resolution)
	for _, r := range resolutions {
		resMap[r.Path] = r
	}

	// Get current server state
	currentFiles, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build current snapshot: %w", err)
	}

	currentMap := make(map[string]FileEntry)
	for _, f := range currentFiles {
		currentMap[f.Path] = f
	}

	// Apply resolutions to create the merged state
	merged := make(map[string]FileEntry)
	for path, f := range currentMap {
		merged[path] = f
	}

	for _, res := range resolutions {
		switch res.Strategy {
		case ResolutionClientWins:
			// In a real implementation, we'd apply the client's content here.
			// For now, we keep the server state and mark it resolved.
			s.logger.Debug("conflict resolved: client wins", "path", res.Path)
		case ResolutionServerWins:
			// Keep server state (already in merged)
			s.logger.Debug("conflict resolved: server wins", "path", res.Path)
		case ResolutionRenameBoth:
			if existing, ok := merged[res.Path]; ok {
				merged[res.NewPath] = existing
				delete(merged, res.Path)
			}
		case ResolutionLastWriteWins:
			// Keep whichever is newer - in practice server state since
			// we haven't received client content yet
			s.logger.Debug("conflict resolved: last-write-wins", "path", res.Path)
		case ResolutionManualMerge:
			// Flag for later UI resolution
			s.logger.Debug("conflict deferred: manual merge", "path", res.Path)
		}
	}

	// Create new snapshot from merged state
	var files []FileEntry
	for _, f := range merged {
		files = append(files, f)
	}

	newSnap, err := s.repo.CreateSnapshot(ctx, snap.DeviceID, snap.UserID, files)
	if err != nil {
		return nil, fmt.Errorf("create new snapshot: %w", err)
	}

	s.logger.Info("conflicts resolved", "snapshot", snapshotID, "newSnapshot", newSnap.ID, "resolutions", len(resolutions))
	return newSnap, nil
}

// GetContent returns raw file content by hash.
func (s *Service) GetContent(hash string) ([]byte, error) {
	return s.contentStore.Read(hash)
}

// buildSnapshotFromDisk walks the source directory and builds a file list.
func (s *Service) buildSnapshotFromDisk(ctx context.Context) ([]FileEntry, error) {
	var files []FileEntry

	err := filepath.WalkDir(s.sourceDir, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() {
			return nil
		}
		if filepath.Ext(path) != ".md" {
			return nil
		}

		info, err := entry.Info()
		if err != nil {
			return err
		}

		relative, err := filepath.Rel(s.sourceDir, path)
		if err != nil {
			return err
		}
		relative = filepath.ToSlash(relative)

		content, err := os.ReadFile(path)
		if err != nil {
			return fmt.Errorf("read file %s: %w", path, err)
		}

		record, err := s.contentStore.PutBytes(content)
		if err != nil {
			return fmt.Errorf("store file %s: %w", path, err)
		}

		files = append(files, FileEntry{
			Path:     relative,
			Hash:     record.Hash,
			Size:     info.Size(),
			Modified: info.ModTime().UTC(),
		})

		return nil
	})
	if err != nil {
		return nil, err
	}

	return files, nil
}
