// Package store contains the local, non-secret Aegis cache.
package store

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

	"aegis/internal/filter"
)

const (
	stateVersion    = 1
	maxArticleBytes = 16 << 20
	maxDraftBytes   = 2 << 20
)

type Article struct {
	Key        string    `json:"key"`
	Group      string    `json:"group"`
	Number     int64     `json:"number"`
	Subject    string    `json:"subject,omitempty"`
	From       string    `json:"from,omitempty"`
	MessageID  string    `json:"message_id,omitempty"`
	Raw        string    `json:"raw,omitempty"`
	Read       bool      `json:"read"`
	Bookmarked bool      `json:"bookmarked"`
	Tags       []string  `json:"tags,omitempty"`
	FetchedAt  time.Time `json:"fetched_at"`
}

type Draft struct {
	ID         string    `json:"id"`
	Groups     string    `json:"groups,omitempty"`
	From       string    `json:"from,omitempty"`
	Subject    string    `json:"subject,omitempty"`
	Body       string    `json:"body,omitempty"`
	References string    `json:"references,omitempty"`
	FollowupTo string    `json:"followup_to,omitempty"`
	UpdatedAt  time.Time `json:"updated_at"`
}

type State struct {
	Version  int                `json:"version"`
	Articles map[string]Article `json:"articles"`
	Drafts   map[string]Draft   `json:"drafts"`
	Filters  []filter.Rule      `json:"filters,omitempty"`
}

type Store struct {
	mu    sync.RWMutex
	path  string
	state State
}

func DefaultPath() (string, error) {
	dir, err := os.UserConfigDir()
	if err != nil {
		return "", fmt.Errorf("locate user configuration directory: %w", err)
	}
	return filepath.Join(dir, "aegis", "state.json"), nil
}

func Open(path string) (*Store, error) {
	if path == "" {
		return nil, errors.New("state path is required")
	}
	s := &Store{path: path, state: emptyState()}
	data, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return s, nil
	}
	if err != nil {
		return nil, fmt.Errorf("read local state: %w", err)
	}
	if len(data) > 32<<20 {
		return nil, errors.New("local state is too large")
	}
	if err := json.Unmarshal(data, &s.state); err != nil {
		return nil, fmt.Errorf("decode local state: %w", err)
	}
	if s.state.Version == 0 {
		s.state.Version = stateVersion
	}
	if s.state.Version != stateVersion {
		return nil, fmt.Errorf("unsupported local state version %d", s.state.Version)
	}
	if s.state.Articles == nil {
		s.state.Articles = make(map[string]Article)
	}
	if s.state.Drafts == nil {
		s.state.Drafts = make(map[string]Draft)
	}
	return s, nil
}

func emptyState() State {
	return State{Version: stateVersion, Articles: make(map[string]Article), Drafts: make(map[string]Draft)}
}

func (s *Store) Save() error {
	s.mu.RLock()
	data, err := json.MarshalIndent(s.state, "", "  ")
	s.mu.RUnlock()
	if err != nil {
		return fmt.Errorf("encode local state: %w", err)
	}
	dir := filepath.Dir(s.path)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("create local state directory: %w", err)
	}
	if err := os.Chmod(dir, 0o700); err != nil {
		return fmt.Errorf("protect local state directory: %w", err)
	}
	tmp, err := os.CreateTemp(dir, ".state-*.tmp")
	if err != nil {
		return fmt.Errorf("create local state temporary file: %w", err)
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)
	if err := tmp.Chmod(0o600); err != nil {
		_ = tmp.Close()
		return err
	}
	if _, err := tmp.Write(append(data, '\n')); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("write local state: %w", err)
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("sync local state: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpName, s.path); err != nil {
		return fmt.Errorf("commit local state: %w", err)
	}
	return nil
}

func (s *Store) Snapshot() State {
	s.mu.RLock()
	defer s.mu.RUnlock()
	result := emptyState()
	result.Filters = append(result.Filters, s.state.Filters...)
	for key, article := range s.state.Articles {
		article.Tags = append([]string(nil), article.Tags...)
		result.Articles[key] = article
	}
	for key, draft := range s.state.Drafts {
		result.Drafts[key] = draft
	}
	return result
}

func (s *Store) UpsertArticle(article Article) error {
	if article.Key == "" {
		return errors.New("article key is required")
	}
	if len(article.Raw) > maxArticleBytes {
		return errors.New("article exceeds local cache limit")
	}
	s.mu.Lock()
	if old, ok := s.state.Articles[article.Key]; ok {
		article.Read = old.Read
		article.Bookmarked = old.Bookmarked
		if len(article.Tags) == 0 {
			article.Tags = append([]string(nil), old.Tags...)
		}
	}
	if article.FetchedAt.IsZero() {
		article.FetchedAt = time.Now().UTC()
	}
	s.state.Articles[article.Key] = article
	s.mu.Unlock()
	return nil
}

func (s *Store) SetRead(key string, read bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	article, ok := s.state.Articles[key]
	if !ok {
		return errors.New("article not found")
	}
	article.Read = read
	s.state.Articles[key] = article
	return nil
}

func (s *Store) SetBookmarked(key string, bookmarked bool) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	article, ok := s.state.Articles[key]
	if !ok {
		return errors.New("article not found")
	}
	article.Bookmarked = bookmarked
	s.state.Articles[key] = article
	return nil
}

func (s *Store) SaveDraft(draft Draft) error {
	if draft.ID == "" {
		return errors.New("draft ID is required")
	}
	if len(draft.Body) > maxDraftBytes {
		return errors.New("draft exceeds local size limit")
	}
	draft.UpdatedAt = time.Now().UTC()
	s.mu.Lock()
	s.state.Drafts[draft.ID] = draft
	s.mu.Unlock()
	return nil
}

func (s *Store) DeleteDraft(id string) {
	s.mu.Lock()
	delete(s.state.Drafts, id)
	s.mu.Unlock()
}

func (s *Store) SetFilters(rules []filter.Rule) {
	s.mu.Lock()
	s.state.Filters = append([]filter.Rule(nil), rules...)
	s.mu.Unlock()
}
