package notes

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"
)

type Note struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Body      string `json:"body"`
	BodyFormats []BodyFormat `json:"bodyFormats"`
	Owner     string `json:"owner"`
	Collaborators []string `json:"collaborators"`
	Color     string `json:"color"`
	UpdatedAt int64  `json:"updatedAt"`
	SortOrder int64  `json:"sortOrder"`
	Pinned    bool   `json:"pinned"`
	Archived  bool   `json:"archived"`
	Deleted   bool   `json:"deleted"`
	Checklist bool     `json:"checklist"`
	Items     []Item   `json:"checklistItems"`
	Labels     []string `json:"labels"`
	ReminderAt *int64  `json:"reminderAt"`
	Images     []Image  `json:"images"`
	Audios     []Audio  `json:"audios"`
}

type Item struct {
	ID      string `json:"id"`
	Text    string `json:"text"`
	Checked bool   `json:"checked"`
}

type BodyFormat struct {
	Start int    `json:"start"`
	End   int    `json:"end"`
	Style string `json:"style"`
}

type Image struct {
	ID       string `json:"id"`
	MimeType string `json:"mimeType"`
	Base64   string `json:"base64"`
}

type Audio struct {
	ID             string `json:"id"`
	MimeType       string `json:"mimeType"`
	Base64         string `json:"base64"`
	DurationMillis int64  `json:"durationMillis"`
}

type Store interface {
	List() []Note
	Upsert(note Note) (Note, error)
	Delete(id string) error
}

type FileStore struct {
	mu    sync.Mutex
	path  string
	notes map[string]Note
}

func OpenFileStore(path string) (*FileStore, error) {
	store := &FileStore{
		path:  path,
		notes: map[string]Note{},
	}
	if err := store.load(); err != nil {
		return nil, err
	}
	return store, nil
}

func (s *FileStore) List() []Note {
	s.mu.Lock()
	defer s.mu.Unlock()

	return sortedNotes(s.notes)
}

func (s *FileStore) Upsert(note Note) (Note, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	note.ID = strings.TrimSpace(note.ID)
	if note.ID == "" {
		return Note{}, errors.New("note id is required")
	}
	if note.Color == "" {
		note.Color = "#FFF475"
	}
	if note.UpdatedAt <= 0 {
		note.UpdatedAt = time.Now().UnixMilli()
	}
	if note.SortOrder <= 0 {
		note.SortOrder = note.UpdatedAt
	}
	note.Owner = strings.TrimSpace(note.Owner)
	note.Collaborators = normalizedStrings(note.Collaborators, 50)

	s.notes[note.ID] = note
	if err := s.persistLocked(); err != nil {
		return Note{}, err
	}
	return note, nil
}

func (s *FileStore) Delete(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	delete(s.notes, strings.TrimSpace(id))
	return s.persistLocked()
}

func (s *FileStore) load() error {
	dir := filepath.Dir(s.path)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("create data directory: %w", err)
	}

	data, err := os.ReadFile(s.path)
	if errors.Is(err, os.ErrNotExist) {
		return s.persistLocked()
	}
	if err != nil {
		return fmt.Errorf("read data file: %w", err)
	}
	if len(strings.TrimSpace(string(data))) == 0 {
		return nil
	}

	var loaded []Note
	if err := json.Unmarshal(data, &loaded); err != nil {
		return fmt.Errorf("decode data file: %w", err)
	}
	for _, note := range loaded {
		if strings.TrimSpace(note.ID) == "" {
			continue
		}
		s.notes[note.ID] = note
	}
	return nil
}

func (s *FileStore) persistLocked() error {
	notes := sortedNotes(s.notes)
	dir := filepath.Dir(s.path)
	tmp, err := os.CreateTemp(dir, ".keeper-notes-*.tmp")
	if err != nil {
		return fmt.Errorf("create temp data file: %w", err)
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)

	encoder := json.NewEncoder(tmp)
	encoder.SetIndent("", "  ")
	if err := encoder.Encode(notes); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("encode data file: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close temp data file: %w", err)
	}

	if err := os.Rename(tmpName, s.path); err != nil {
		_ = os.Remove(s.path)
		if retryErr := os.Rename(tmpName, s.path); retryErr != nil {
			return fmt.Errorf("replace data file: %w", retryErr)
		}
	}
	return nil
}

func normalizedStrings(values []string, limit int) []string {
	seen := map[string]struct{}{}
	normalized := []string{}
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		key := strings.ToLower(trimmed)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, trimmed)
		if len(normalized) >= limit {
			break
		}
	}
	return normalized
}

func sortedNotes(noteMap map[string]Note) []Note {
	notes := make([]Note, 0, len(noteMap))
	for _, note := range noteMap {
		notes = append(notes, note)
	}
	sort.Slice(notes, func(i, j int) bool {
		leftOrder := notes[i].SortOrder
		if leftOrder <= 0 {
			leftOrder = notes[i].UpdatedAt
		}
		rightOrder := notes[j].SortOrder
		if rightOrder <= 0 {
			rightOrder = notes[j].UpdatedAt
		}
		if leftOrder == rightOrder {
			if notes[i].UpdatedAt != notes[j].UpdatedAt {
				return notes[i].UpdatedAt > notes[j].UpdatedAt
			}
			return notes[i].ID < notes[j].ID
		}
		return leftOrder > rightOrder
	})
	return notes
}
