package platform

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"sync"
)

// ClipboardEvent is one observed change of the local clipboard.
type ClipboardEvent struct {
	Text string
	// Sensitive is set by the platform Source when the pasteboard carries a
	// privacy marker — on macOS org.nspasteboard.ConcealedType / TransientType /
	// AutoGeneratedType or a known password-manager UTI; on Windows the
	// ExcludeClipboardContentFromMonitorProcessing / CanUploadToCloudClipboard
	// formats. Sensitive events are never uploaded.
	Sensitive bool
}

// Sink receives clipboard text that the policy decided should be uploaded.
type Sink func(text string)

// Source is the platform-specific clipboard backend: it watches for local
// changes and can write text back. Implementations live in clipboard_darwin.go
// / clipboard_windows.go. The upstream golang.design/x/clipboard library only
// surfaces the changed text (no change-count, no pasteboard types), so the
// Sensitive flag and the upload policy below are this package's responsibility,
// not the library's.
type Source interface {
	// Watch emits an event on every observed local clipboard change until ctx
	// is cancelled, then closes the channel. The implementation sets Sensitive
	// from the pasteboard's privacy markers.
	Watch(ctx context.Context) <-chan ClipboardEvent
	// Write replaces the local clipboard text.
	Write(text string) error
}

// Monitor applies cdrop's upload policy on top of a raw clipboard event stream:
// it drops sensitive content, de-duplicates repeats, and suppresses the echo of
// our own writes (the loop guard, PLAN.md R4). Everything here is pure and
// host-independent, so it is unit-tested without a real clipboard; the
// platform Source produces the raw events and performs the OS writes.
//
// Debounce of rapid successive copies is intentionally left to the upload
// callback (wrap it with github.com/bep/debounce in the app wiring) so the
// policy stays free of timers and trivially testable.
type Monitor struct {
	upload Sink

	mu       sync.Mutex
	lastHash string // hash of the last text we accepted (dedup)
	selfHash string // hash of the text we most recently wrote ourselves (loop guard)
}

// NewMonitor builds a Monitor that forwards accepted text to upload.
func NewMonitor(upload Sink) *Monitor {
	return &Monitor{upload: upload}
}

// Observe runs the policy for one raw event. It returns true when the event was
// accepted and forwarded to the sink, false when it was filtered out. Safe to
// call concurrently with ApplyRemote — the watch goroutine and the download
// path share one Monitor.
func (m *Monitor) Observe(ev ClipboardEvent) bool {
	if ev.Sensitive || ev.Text == "" {
		return false
	}
	h := hashText(ev.Text)

	m.mu.Lock()
	if h == m.selfHash {
		// Echo of our own write: consume it once (don't re-upload) and disarm,
		// so a genuine later re-copy of the same text still passes.
		m.selfHash = ""
		m.lastHash = h
		m.mu.Unlock()
		return false
	}
	if h == m.lastHash {
		m.mu.Unlock()
		return false // unchanged content re-observed
	}
	m.lastHash = h
	m.mu.Unlock()

	m.upload(ev.Text) // outside the lock: the sink may block (network / IPC)
	return true
}

// WillWrite must be called immediately before the Source writes text to the
// local clipboard, so the resulting change notification (carrying the same
// text) is recognised as our own echo and not re-uploaded.
func (m *Monitor) WillWrite(text string) {
	m.mu.Lock()
	m.selfHash = hashText(text)
	m.mu.Unlock()
}

// ApplyRemote writes a remote clipboard change to the local clipboard (the
// download direction), skipping our own uploads echoed back by the server and
// content we already hold. It arms the loop guard before writing, so the
// Source's resulting change event is not re-uploaded. Returns true if it wrote.
func (m *Monitor) ApplyRemote(src Source, cc ClipboardContent, selfDevice string) bool {
	if cc.Content == "" {
		return false
	}
	if selfDevice != "" && cc.SourceDevice == selfDevice {
		return false // our own upload, echoed back by the server
	}
	h := hashText(cc.Content)

	m.mu.Lock()
	if h == m.lastHash {
		m.mu.Unlock()
		return false // already the current local content
	}
	m.selfHash = h // arm loop guard inline (avoid re-locking via WillWrite)
	m.mu.Unlock()

	if err := src.Write(cc.Content); err != nil {
		return false
	}

	m.mu.Lock()
	m.lastHash = h
	m.mu.Unlock()
	return true
}

// Run drives src's events through m until ctx is cancelled or the source closes.
// It is the glue the app uses; all policy decisions live in Monitor.
func Run(ctx context.Context, src Source, m *Monitor) {
	ch := src.Watch(ctx)
	for {
		select {
		case <-ctx.Done():
			return
		case ev, ok := <-ch:
			if !ok {
				return
			}
			m.Observe(ev)
		}
	}
}

func hashText(s string) string {
	sum := sha256.Sum256([]byte(s))
	return hex.EncodeToString(sum[:])
}
