package main

import (
	"encoding/json"
	"errors"
	"os"
	"sort"
	"sync"
	"time"
)

var ErrNotFound = errors.New("not found")

// snapshot — всё состояние приложения, сериализуемое в JSON-файл.
type snapshot struct {
	Clubs    []*Club    `json:"clubs"`
	Members  []*Member  `json:"members"`
	Books    []*Book    `json:"books"`
	Meetings []*Meeting `json:"meetings"`
	Posts    []*Post    `json:"posts"`
}

// Store держит состояние в памяти и периодически сбрасывает его на диск.
// За интерфейсом ниже легко заменить на SQLite/Postgres, не трогая хендлеры.
type Store struct {
	mu   sync.RWMutex
	path string

	clubs    map[string]*Club
	members  map[string]*Member
	books    map[string]*Book
	meetings map[string]*Meeting
	posts    map[string]*Post
}

func NewStore(path string) *Store {
	s := &Store{
		path:     path,
		clubs:    map[string]*Club{},
		members:  map[string]*Member{},
		books:    map[string]*Book{},
		meetings: map[string]*Meeting{},
		posts:    map[string]*Post{},
	}
	s.load()
	if len(s.clubs) == 0 {
		s.seed()
	}
	return s
}

// ---- персистентность ----

func (s *Store) load() {
	data, err := os.ReadFile(s.path)
	if err != nil {
		return // файла нет — стартуем с пустого состояния
	}
	var snap snapshot
	if err := json.Unmarshal(data, &snap); err != nil {
		return
	}
	for _, c := range snap.Clubs {
		s.clubs[c.ID] = c
	}
	for _, m := range snap.Members {
		s.members[m.ID] = m
	}
	for _, b := range snap.Books {
		s.books[b.ID] = b
	}
	for _, m := range snap.Meetings {
		s.meetings[m.ID] = m
	}
	for _, p := range snap.Posts {
		s.posts[p.ID] = p
	}
}

// persist вызывается под write-lock'ом.
func (s *Store) persist() {
	snap := snapshot{}
	for _, c := range s.clubs {
		snap.Clubs = append(snap.Clubs, c)
	}
	for _, m := range s.members {
		snap.Members = append(snap.Members, m)
	}
	for _, b := range s.books {
		snap.Books = append(snap.Books, b)
	}
	for _, m := range s.meetings {
		snap.Meetings = append(snap.Meetings, m)
	}
	for _, p := range s.posts {
		snap.Posts = append(snap.Posts, p)
	}
	data, err := json.MarshalIndent(snap, "", "  ")
	if err != nil {
		return
	}
	tmp := s.path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o644); err == nil {
		_ = os.Rename(tmp, s.path) // атомарная замена
	}
}

// ---- Клубы ----

func (s *Store) ListClubs() []*Club {
	s.mu.RLock()
	defer s.mu.RUnlock()
	out := make([]*Club, 0, len(s.clubs))
	for _, c := range s.clubs {
		out = append(out, c)
	}
	sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) })
	return out
}

func (s *Store) GetClub(id string) (*Club, error) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	c, ok := s.clubs[id]
	if !ok {
		return nil, ErrNotFound
	}
	return c, nil
}

func (s *Store) CreateClub(name, description string) *Club {
	s.mu.Lock()
	defer s.mu.Unlock()
	c := &Club{ID: newID(), Name: name, Description: description, CreatedAt: time.Now()}
	s.clubs[c.ID] = c
	s.persist()
	return c
}

// ---- Участники ----

func (s *Store) ListMembers(clubID string) []*Member {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []*Member
	for _, m := range s.members {
		if m.ClubID == clubID {
			out = append(out, m)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].JoinedAt.Before(out[j].JoinedAt) })
	return out
}

func (s *Store) JoinClub(clubID, name string) (*Member, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.clubs[clubID]; !ok {
		return nil, ErrNotFound
	}
	// не добавляем дубликат по имени
	for _, m := range s.members {
		if m.ClubID == clubID && m.Name == name {
			return m, nil
		}
	}
	m := &Member{ID: newID(), ClubID: clubID, Name: name, JoinedAt: time.Now()}
	s.members[m.ID] = m
	s.persist()
	return m, nil
}

// ---- Книги ----

func (s *Store) ListBooks(clubID string) []*Book {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []*Book
	for _, b := range s.books {
		if b.ClubID == clubID {
			out = append(out, b)
		}
	}
	// сортировка: сначала по числу голосов, затем по дате
	sort.Slice(out, func(i, j int) bool {
		if len(out[i].Votes) != len(out[j].Votes) {
			return len(out[i].Votes) > len(out[j].Votes)
		}
		return out[i].CreatedAt.After(out[j].CreatedAt)
	})
	return out
}

func (s *Store) AddBook(clubID, title, author, addedBy string) (*Book, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.clubs[clubID]; !ok {
		return nil, ErrNotFound
	}
	b := &Book{
		ID:        newID(),
		ClubID:    clubID,
		Title:     title,
		Author:    author,
		Status:    StatusSuggested,
		AddedBy:   addedBy,
		Votes:     []string{},
		CreatedAt: time.Now(),
	}
	s.books[b.ID] = b
	s.persist()
	return b, nil
}

// ToggleVote добавляет/снимает голос участника за книгу.
func (s *Store) ToggleVote(bookID, voter string) (*Book, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	b, ok := s.books[bookID]
	if !ok {
		return nil, ErrNotFound
	}
	for i, v := range b.Votes {
		if v == voter {
			b.Votes = append(b.Votes[:i], b.Votes[i+1:]...)
			s.persist()
			return b, nil
		}
	}
	b.Votes = append(b.Votes, voter)
	s.persist()
	return b, nil
}

func (s *Store) SetBookStatus(bookID string, status BookStatus) (*Book, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	b, ok := s.books[bookID]
	if !ok {
		return nil, ErrNotFound
	}
	b.Status = status
	s.persist()
	return b, nil
}

// ---- Встречи ----

func (s *Store) ListMeetings(clubID string) []*Meeting {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []*Meeting
	for _, m := range s.meetings {
		if m.ClubID == clubID {
			out = append(out, m)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].StartsAt.Before(out[j].StartsAt) })
	return out
}

func (s *Store) CreateMeeting(clubID, bookID, title string, startsAt time.Time, location, notes string) (*Meeting, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.clubs[clubID]; !ok {
		return nil, ErrNotFound
	}
	m := &Meeting{
		ID:        newID(),
		ClubID:    clubID,
		BookID:    bookID,
		Title:     title,
		StartsAt:  startsAt,
		Location:  location,
		Notes:     notes,
		CreatedAt: time.Now(),
	}
	s.meetings[m.ID] = m
	s.persist()
	return m, nil
}

// ---- Обсуждение ----

func (s *Store) ListPosts(clubID string) []*Post {
	s.mu.RLock()
	defer s.mu.RUnlock()
	var out []*Post
	for _, p := range s.posts {
		if p.ClubID == clubID {
			out = append(out, p)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.Before(out[j].CreatedAt) })
	return out
}

func (s *Store) AddPost(clubID, author, body string) (*Post, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.clubs[clubID]; !ok {
		return nil, ErrNotFound
	}
	p := &Post{ID: newID(), ClubID: clubID, Author: author, Body: body, CreatedAt: time.Now()}
	s.posts[p.ID] = p
	s.persist()
	return p, nil
}

// seed наполняет хранилище демо-данными при первом запуске.
func (s *Store) seed() {
	s.mu.Lock()
	defer s.mu.Unlock()
	now := time.Now()
	club := &Club{ID: newID(), Name: "Клуб научной фантастики", Description: "Читаем и обсуждаем sci-fi: от классики до новинок.", CreatedAt: now}
	s.clubs[club.ID] = club

	members := []string{"Аня", "Борис", "Вера"}
	for i, name := range members {
		m := &Member{ID: newID(), ClubID: club.ID, Name: name, JoinedAt: now.Add(time.Duration(i) * time.Minute)}
		s.members[m.ID] = m
	}

	reading := &Book{ID: newID(), ClubID: club.ID, Title: "Задача трёх тел", Author: "Лю Цысинь", Status: StatusReading, AddedBy: "Аня", Votes: []string{"Аня", "Борис"}, CreatedAt: now}
	dune := &Book{ID: newID(), ClubID: club.ID, Title: "Дюна", Author: "Фрэнк Герберт", Status: StatusSuggested, AddedBy: "Борис", Votes: []string{"Борис"}, CreatedAt: now}
	hyperion := &Book{ID: newID(), ClubID: club.ID, Title: "Гиперион", Author: "Дэн Симмонс", Status: StatusSuggested, AddedBy: "Вера", Votes: []string{"Вера", "Аня", "Борис"}, CreatedAt: now}
	for _, b := range []*Book{reading, dune, hyperion} {
		s.books[b.ID] = b
	}

	meeting := &Meeting{
		ID: newID(), ClubID: club.ID, BookID: reading.ID,
		Title: "Обсуждение первых глав", StartsAt: now.Add(72 * time.Hour),
		Location: "Zoom", Notes: "Главы 1–10, готовим вопросы.", CreatedAt: now,
	}
	s.meetings[meeting.ID] = meeting

	post := &Post{ID: newID(), ClubID: club.ID, Author: "Аня", Body: "Всем привет! Начинаем читать «Задачу трёх тел».", CreatedAt: now}
	s.posts[post.ID] = post

	s.persist()
}
