package sync

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

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

var (
	ErrForbidden       = errors.New("sync snapshot does not belong to user")
	ErrContentRequired = errors.New("sync content is required")
	ErrHashMismatch    = errors.New("sync content hash mismatch")
	ErrInvalidPath     = errors.New("invalid sync path")
	ErrInvalidHash     = errors.New("invalid sync content hash")
	ErrContentNotFound = errors.New("sync content hash not found")
)

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

type FileAccessFunc func(FileEntry) bool
type PathAccessFunc func(string) bool

// 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) {
	return s.InitSyncFiltered(ctx, deviceID, userID, nil)
}

func (s *Service) InitSyncFiltered(ctx context.Context, deviceID, userID string, canRead FileAccessFunc) (*Snapshot, error) {
	files, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build snapshot: %w", err)
	}
	files = filterFiles(files, canRead)

	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, userID string, clientDelta Delta) (*DeltaResult, error) {
	return s.ApplyDeltaFiltered(ctx, snapshotID, userID, clientDelta, nil, nil)
}

func (s *Service) ApplyDeltaFiltered(ctx context.Context, snapshotID, userID string, clientDelta Delta, canRead FileAccessFunc, canWrite PathAccessFunc) (*DeltaResult, error) {
	snap, err := s.repo.GetSnapshot(ctx, snapshotID)
	if err != nil {
		return nil, fmt.Errorf("get snapshot: %w", err)
	}
	if snap.UserID != userID {
		return nil, ErrForbidden
	}
	for _, clientChange := range clientDelta.Changes {
		if canWrite != nil && !canWrite(clientChange.Path) {
			return nil, ErrForbidden
		}
		if clientChange.OldPath != "" && canWrite != nil && !canWrite(clientChange.OldPath) {
			return nil, ErrForbidden
		}
	}

	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)
	}
	currentFiles = filterFiles(currentFiles, canRead)

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

	serverDelta := make([]Change, 0)
	conflicts := make([]Conflict, 0)

	// 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
	conflictPaths := make(map[string]struct{})
	for _, clientChange := range clientDelta.Changes {
		if clientChange.Type == ChangeCreate || clientChange.Type == ChangeUpdate || clientChange.Type == ChangeDelete {
			serverCurrent, serverHas := currentMap[clientChange.Path]
			serverOld, serverHad := serverFiles[clientChange.Path]

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

	for _, clientChange := range clientDelta.Changes {
		if _, conflicted := conflictPaths[clientChange.Path]; conflicted {
			continue
		}
		if err := s.applyClientChange(ctx, clientChange); err != nil {
			return nil, err
		}
	}

	if len(clientDelta.Changes) > 0 {
		if _, err := s.docService.SyncSourceDir(ctx); err != nil {
			return nil, fmt.Errorf("sync documents after client delta: %w", err)
		}
	}

	latestFiles, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build post-delta snapshot: %w", err)
	}
	latestFiles = filterFiles(latestFiles, canRead)
	newSnap, err := s.repo.CreateSnapshot(ctx, snap.DeviceID, snap.UserID, latestFiles)
	if err != nil {
		return nil, fmt.Errorf("create post-delta snapshot: %w", err)
	}

	return &DeltaResult{
		ServerDelta:   serverDelta,
		Conflicts:     conflicts,
		NewSnapshotID: newSnap.ID,
	}, nil
}

// ResolveConflicts applies resolved changes and creates a new snapshot.
func (s *Service) ResolveConflicts(ctx context.Context, snapshotID, userID string, resolutions []Resolution) (*Snapshot, error) {
	return s.ResolveConflictsFiltered(ctx, snapshotID, userID, resolutions, nil, nil)
}

func (s *Service) ResolveConflictsFiltered(ctx context.Context, snapshotID, userID string, resolutions []Resolution, canRead FileAccessFunc, canWrite PathAccessFunc) (*Snapshot, error) {
	snap, err := s.repo.GetSnapshot(ctx, snapshotID)
	if err != nil {
		return nil, fmt.Errorf("get snapshot: %w", err)
	}
	if snap.UserID != userID {
		return nil, ErrForbidden
	}
	for _, resolution := range resolutions {
		if canWrite != nil && !canWrite(resolution.Path) {
			return nil, ErrForbidden
		}
		if resolution.NewPath != "" && canWrite != nil && !canWrite(resolution.NewPath) {
			return nil, ErrForbidden
		}
	}

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

	for _, res := range resolutions {
		switch res.Strategy {
		case ResolutionClientWins:
			if err := s.applyResolvedContent(res.Path, res.Hash, res.Content); err != nil {
				return nil, err
			}
			s.logger.Debug("conflict resolved: client wins", "path", res.Path)
		case ResolutionServerWins:
			s.logger.Debug("conflict resolved: server wins", "path", res.Path)
		case ResolutionRenameBoth:
			if res.NewPath == "" {
				res.NewPath = conflictPath(res.Path)
			}
			if err := s.applyResolvedContent(res.NewPath, res.Hash, res.Content); err != nil {
				return nil, err
			}
		case ResolutionLastWriteWins:
			if res.Content != "" {
				if err := s.applyResolvedContent(res.Path, res.Hash, res.Content); err != nil {
					return nil, err
				}
			}
		case ResolutionManualMerge:
			if res.Content != "" {
				if err := s.applyResolvedContent(res.Path, res.Hash, res.Content); err != nil {
					return nil, err
				}
			}
			s.logger.Debug("conflict deferred: manual merge", "path", res.Path)
		}
	}

	if len(resolutions) > 0 {
		if _, err := s.docService.SyncSourceDir(ctx); err != nil {
			return nil, fmt.Errorf("sync documents after resolutions: %w", err)
		}
	}

	files, err := s.buildSnapshotFromDisk(ctx)
	if err != nil {
		return nil, fmt.Errorf("build resolved snapshot: %w", err)
	}
	files = filterFiles(files, canRead)

	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
}

func filterFiles(files []FileEntry, canRead FileAccessFunc) []FileEntry {
	if canRead == nil {
		return files
	}
	filtered := make([]FileEntry, 0, len(files))
	for _, file := range files {
		if canRead(file) {
			filtered = append(filtered, file)
		}
	}
	return filtered
}

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

func (s *Service) applyClientChange(_ context.Context, change Change) error {
	switch change.Type {
	case ChangeCreate, ChangeUpdate:
		return s.applyResolvedContent(change.Path, change.Hash, change.Content)
	case ChangeDelete:
		path, err := s.safeContentPath(change.Path)
		if err != nil {
			return err
		}
		if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("delete synced file %s: %w", change.Path, err)
		}
		return nil
	case ChangeRename:
		oldPath, err := s.safeContentPath(change.OldPath)
		if err != nil {
			return err
		}
		newPath, err := s.safeContentPath(change.Path)
		if err != nil {
			return err
		}
		if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
			return fmt.Errorf("create rename directory %s: %w", change.Path, err)
		}
		if err := os.Rename(oldPath, newPath); err != nil && !errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("rename synced file %s to %s: %w", change.OldPath, change.Path, err)
		}
		if change.Content != "" || change.Hash != "" {
			return s.applyResolvedContent(change.Path, change.Hash, change.Content)
		}
		return nil
	default:
		return nil
	}
}

func (s *Service) applyResolvedContent(requestPath, expectedHash, content string) error {
	path, err := s.safeContentPath(requestPath)
	if err != nil {
		return err
	}

	var bytes []byte
	if content != "" {
		record, err := s.contentStore.PutBytes([]byte(content))
		if err != nil {
			return fmt.Errorf("store synced content %s: %w", requestPath, err)
		}
		if expectedHash != "" && record.Hash != expectedHash {
			return fmt.Errorf("%w: %s", ErrHashMismatch, requestPath)
		}
		bytes = []byte(content)
	} else {
		if expectedHash == "" {
			return fmt.Errorf("%w: %s", ErrContentRequired, requestPath)
		}
		if !isSHA256Hex(expectedHash) {
			return fmt.Errorf("%w: %s", ErrInvalidHash, expectedHash)
		}
		bytes, err = s.contentStore.Read(expectedHash)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				return fmt.Errorf("%w: %s", ErrContentNotFound, expectedHash)
			}
			return fmt.Errorf("read synced content %s: %w", expectedHash, err)
		}
	}

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create synced file directory %s: %w", requestPath, err)
	}
	if err := os.WriteFile(path, bytes, 0o644); err != nil {
		return fmt.Errorf("write synced file %s: %w", requestPath, err)
	}
	return nil
}

func (s *Service) safeContentPath(requestPath string) (string, error) {
	if requestPath == "" {
		return "", ErrInvalidPath
	}
	clean := filepath.ToSlash(filepath.Clean(filepath.FromSlash(strings.Trim(requestPath, "/"))))
	if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") || filepath.IsAbs(clean) {
		return "", ErrInvalidPath
	}
	if !strings.HasSuffix(strings.ToLower(clean), ".md") {
		return "", ErrInvalidPath
	}
	sourceRoot, err := filepath.Abs(s.sourceDir)
	if err != nil {
		return "", fmt.Errorf("resolve source root: %w", err)
	}
	target, err := filepath.Abs(filepath.Join(sourceRoot, filepath.FromSlash(clean)))
	if err != nil {
		return "", ErrInvalidPath
	}
	relative, err := filepath.Rel(sourceRoot, target)
	if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
		return "", ErrInvalidPath
	}
	return target, nil
}

func conflictPath(path string) string {
	ext := filepath.Ext(path)
	stem := strings.TrimSuffix(path, ext)
	return stem + " (conflict)" + ext
}

func isSHA256Hex(hash string) bool {
	if len(hash) != 64 {
		return false
	}
	for _, c := range hash {
		if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
			return false
		}
	}
	return true
}

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

	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 strings.ToLower(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
}
