package main

import (
	"context"
	"encoding/json"
	"flag"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/google/uuid"
)

// ============================================
// TASKS
// ============================================

type Task struct {
	ID        string    `json:"id"`
	Text      string    `json:"text,omitempty"`
	Title     string    `json:"title,omitempty"`
	Priority  string    `json:"priority"`
	Status    string    `json:"status,omitempty"`
	Owner     string    `json:"owner,omitempty"`  // "johan" or "james"
	Domain    string    `json:"domain,omitempty"` // Kaseya, inou, Sophia, ClawdNode, Infrastructure, etc.
	Notes     string    `json:"notes,omitempty"`
	CreatedAt time.Time `json:"created_at,omitempty"`
	Updated   string    `json:"updated,omitempty"`
}

type TaskStore struct {
	Tasks []Task `json:"tasks"`
	mu    sync.RWMutex
	path  string
}

func NewTaskStore(path string) (*TaskStore, error) {
	store := &TaskStore{path: path, Tasks: []Task{}}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Tasks []Task `json:"tasks"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Tasks != nil {
		store.Tasks = wrapper.Tasks
		return store, nil
	}

	if err := json.Unmarshal(data, &store.Tasks); err != nil {
		store.Tasks = []Task{}
		return store, store.save()
	}

	return store, nil
}

func (s *TaskStore) save() error {
	wrapper := struct {
		Tasks []Task `json:"tasks"`
	}{Tasks: s.Tasks}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *TaskStore) Add(task Task) (Task, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	task.ID = uuid.New().String()[:8]
	task.CreatedAt = time.Now()
	if task.Priority == "" {
		task.Priority = "medium"
	}
	if task.Status == "" {
		task.Status = "pending"
	}
	if task.Owner == "" {
		task.Owner = "james"
	}

	s.Tasks = append(s.Tasks, task)
	return task, s.save()
}

func (s *TaskStore) Update(id string, updates Task) (Task, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, task := range s.Tasks {
		if task.ID == id {
			if updates.Title != "" {
				s.Tasks[i].Title = updates.Title
			}
			if updates.Text != "" {
				s.Tasks[i].Text = updates.Text
			}
			if updates.Priority != "" {
				s.Tasks[i].Priority = updates.Priority
			}
			if updates.Status != "" {
				s.Tasks[i].Status = updates.Status
			}
			if updates.Owner != "" {
				s.Tasks[i].Owner = updates.Owner
			}
			if updates.Domain != "" {
				s.Tasks[i].Domain = updates.Domain
			}
			if updates.Notes != "" {
				s.Tasks[i].Notes = updates.Notes
			}
			s.Tasks[i].Updated = time.Now().Format(time.RFC3339)
			s.save()
			return s.Tasks[i], true
		}
	}
	return Task{}, false
}

func (s *TaskStore) List() []Task {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.Tasks
}

func (s *TaskStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, task := range s.Tasks {
		if task.ID == id {
			s.Tasks = append(s.Tasks[:i], s.Tasks[i+1:]...)
			s.save()
			return true
		}
	}
	return false
}

// ============================================
// NEWS
// ============================================

type NewsItem struct {
	ID        string    `json:"id"`
	Title     string    `json:"title"`
	Body      string    `json:"body"`
	Type      string    `json:"type"` // info, success, warning, error
	Source    string    `json:"source,omitempty"`
	URL       string    `json:"url,omitempty"`
	Topic     string    `json:"topic,omitempty"`
	Timestamp time.Time `json:"timestamp"`
}

type NewsStore struct {
	Items []NewsItem `json:"items"`
	mu    sync.RWMutex
	path  string
}

func NewNewsStore(path string) (*NewsStore, error) {
	store := &NewsStore{path: path, Items: []NewsItem{}}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Items []NewsItem `json:"items"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Items != nil {
		store.Items = wrapper.Items
		return store, nil
	}

	return store, store.save()
}

func (s *NewsStore) save() error {
	wrapper := struct {
		Items []NewsItem `json:"items"`
	}{Items: s.Items}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *NewsStore) Add(item NewsItem) (NewsItem, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	item.ID = uuid.New().String()[:8]
	item.Timestamp = time.Now()
	if item.Type == "" {
		item.Type = "info"
	}

	// Prepend (newest first)
	s.Items = append([]NewsItem{item}, s.Items...)
	
	// Keep only last 50 items
	if len(s.Items) > 50 {
		s.Items = s.Items[:50]
	}
	
	return item, s.save()
}

func (s *NewsStore) List(limit int) []NewsItem {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if limit > 0 && limit < len(s.Items) {
		return s.Items[:limit]
	}
	return s.Items
}

func (s *NewsStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, item := range s.Items {
		if item.ID == id {
			s.Items = append(s.Items[:i], s.Items[i+1:]...)
			s.save()
			return true
		}
	}
	return false
}

func (s *NewsStore) Clear() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.Items = []NewsItem{}
	return s.save()
}

// ============================================
// BRIEFINGS
// ============================================

type Briefing struct {
	ID        string    `json:"id"`
	Date      string    `json:"date"`
	Title     string    `json:"title"`
	Weather   string    `json:"weather,omitempty"`
	Markets   string    `json:"markets,omitempty"`
	News      string    `json:"news,omitempty"`
	Tasks     string    `json:"tasks,omitempty"`
	Summary   string    `json:"summary,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type BriefingStore struct {
	Briefings []Briefing `json:"briefings"`
	mu        sync.RWMutex
	path      string
}

func NewBriefingStore(path string) (*BriefingStore, error) {
	store := &BriefingStore{path: path, Briefings: []Briefing{}}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Briefings []Briefing `json:"briefings"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Briefings != nil {
		store.Briefings = wrapper.Briefings
		return store, nil
	}

	return store, store.save()
}

func (s *BriefingStore) save() error {
	wrapper := struct {
		Briefings []Briefing `json:"briefings"`
	}{Briefings: s.Briefings}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *BriefingStore) Add(b Briefing) (Briefing, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	b.ID = uuid.New().String()[:8]
	b.CreatedAt = time.Now()
	if b.Date == "" {
		b.Date = time.Now().Format("2006-01-02")
	}
	if b.Title == "" {
		b.Title = "Morning Brief"
	}

	s.Briefings = append(s.Briefings, b)
	return b, s.save()
}

func (s *BriefingStore) List(limit int) []Briefing {
	s.mu.RLock()
	defer s.mu.RUnlock()

	// Sort by date descending
	sorted := make([]Briefing, len(s.Briefings))
	copy(sorted, s.Briefings)
	sort.Slice(sorted, func(i, j int) bool {
		return sorted[i].CreatedAt.After(sorted[j].CreatedAt)
	})

	if limit > 0 && limit < len(sorted) {
		return sorted[:limit]
	}
	return sorted
}

func (s *BriefingStore) Get(id string) (Briefing, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	for _, b := range s.Briefings {
		if b.ID == id {
			return b, true
		}
	}
	return Briefing{}, false
}

func (s *BriefingStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, b := range s.Briefings {
		if b.ID == id {
			s.Briefings = append(s.Briefings[:i], s.Briefings[i+1:]...)
			s.save()
			return true
		}
	}
	return false
}

// ============================================
// DELIVERIES
// ============================================

type Delivery struct {
	ID             string    `json:"id"`
	Carrier        string    `json:"carrier,omitempty"`        // UPS, FedEx, USPS, etc.
	Retailer       string    `json:"retailer,omitempty"`       // Amazon, Nordstrom, etc.
	Description    string    `json:"description"`              // What's in the package
	TrackingNumber string    `json:"tracking_number,omitempty"`
	TrackingURL    string    `json:"tracking_url,omitempty"`
	ExpectedDate   string    `json:"expected_date,omitempty"`  // "2026-02-03" or "Monday"
	Status         string    `json:"status"`                   // shipped, in_transit, out_for_delivery, delayed, delivered
	Notes          string    `json:"notes,omitempty"`
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

type DeliveryStore struct {
	Deliveries []Delivery `json:"deliveries"`
	mu         sync.RWMutex
	path       string
}

func NewDeliveryStore(path string) (*DeliveryStore, error) {
	store := &DeliveryStore{path: path, Deliveries: []Delivery{}}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Deliveries []Delivery `json:"deliveries"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Deliveries != nil {
		store.Deliveries = wrapper.Deliveries
		return store, nil
	}

	return store, store.save()
}

func (s *DeliveryStore) save() error {
	wrapper := struct {
		Deliveries []Delivery `json:"deliveries"`
	}{Deliveries: s.Deliveries}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *DeliveryStore) Add(d Delivery) (Delivery, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	d.ID = uuid.New().String()[:8]
	d.CreatedAt = time.Now()
	d.UpdatedAt = time.Now()
	if d.Status == "" {
		d.Status = "shipped"
	}

	s.Deliveries = append(s.Deliveries, d)
	return d, s.save()
}

func (s *DeliveryStore) Update(id string, updates Delivery) (Delivery, bool) {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, d := range s.Deliveries {
		if d.ID == id {
			if updates.Carrier != "" {
				s.Deliveries[i].Carrier = updates.Carrier
			}
			if updates.Retailer != "" {
				s.Deliveries[i].Retailer = updates.Retailer
			}
			if updates.Description != "" {
				s.Deliveries[i].Description = updates.Description
			}
			if updates.TrackingNumber != "" {
				s.Deliveries[i].TrackingNumber = updates.TrackingNumber
			}
			if updates.TrackingURL != "" {
				s.Deliveries[i].TrackingURL = updates.TrackingURL
			}
			if updates.ExpectedDate != "" {
				s.Deliveries[i].ExpectedDate = updates.ExpectedDate
			}
			if updates.Status != "" {
				s.Deliveries[i].Status = updates.Status
			}
			if updates.Notes != "" {
				s.Deliveries[i].Notes = updates.Notes
			}
			s.Deliveries[i].UpdatedAt = time.Now()
			s.save()
			return s.Deliveries[i], true
		}
	}
	return Delivery{}, false
}

func (s *DeliveryStore) List() []Delivery {
	s.mu.RLock()
	defer s.mu.RUnlock()
	
	// Return non-delivered items, sorted by expected date
	var active []Delivery
	for _, d := range s.Deliveries {
		if d.Status != "delivered" {
			active = append(active, d)
		}
	}
	
	sort.Slice(active, func(i, j int) bool {
		return active[i].ExpectedDate < active[j].ExpectedDate
	})
	
	return active
}

func (s *DeliveryStore) ListAll() []Delivery {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.Deliveries
}

// Upsert finds an existing delivery by tracking_number (exact match) or
// description+retailer (fuzzy), then updates it. If no match, creates new.
// Returns the delivery, whether it was an update (vs create), and any error.
func (s *DeliveryStore) Upsert(d Delivery) (Delivery, bool, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 1. Match by tracking_number (strongest signal)
	if d.TrackingNumber != "" {
		for i, existing := range s.Deliveries {
			if existing.TrackingNumber == d.TrackingNumber {
				s.mergeDelivery(i, d)
				s.save()
				return s.Deliveries[i], true, nil
			}
		}
	}

	// 2. Match by retailer + similar description (for orders without tracking yet)
	if d.Retailer != "" && d.Description != "" {
		for i, existing := range s.Deliveries {
			if existing.Retailer == d.Retailer && existing.Status != "delivered" &&
				deliveryDescSimilar(existing.Description, d.Description) {
				s.mergeDelivery(i, d)
				s.save()
				return s.Deliveries[i], true, nil
			}
		}
	}

	// 3. No match — create new
	d.ID = uuid.New().String()[:8]
	d.CreatedAt = time.Now()
	d.UpdatedAt = time.Now()
	if d.Status == "" {
		d.Status = "shipped"
	}
	s.Deliveries = append(s.Deliveries, d)
	return d, false, s.save()
}

// mergeDelivery updates existing delivery fields with non-empty values from the update.
// Status always progresses forward (shipped→in_transit→out_for_delivery→delivered), never backward.
func (s *DeliveryStore) mergeDelivery(i int, u Delivery) {
	if u.Carrier != "" {
		s.Deliveries[i].Carrier = u.Carrier
	}
	if u.Retailer != "" {
		s.Deliveries[i].Retailer = u.Retailer
	}
	if u.Description != "" {
		s.Deliveries[i].Description = u.Description
	}
	if u.TrackingNumber != "" {
		s.Deliveries[i].TrackingNumber = u.TrackingNumber
	}
	if u.TrackingURL != "" {
		s.Deliveries[i].TrackingURL = u.TrackingURL
	}
	if u.ExpectedDate != "" {
		s.Deliveries[i].ExpectedDate = u.ExpectedDate
	}
	if u.Status != "" && deliveryStatusRank(u.Status) >= deliveryStatusRank(s.Deliveries[i].Status) {
		s.Deliveries[i].Status = u.Status
	}
	if u.Notes != "" {
		s.Deliveries[i].Notes = u.Notes
	}
	s.Deliveries[i].UpdatedAt = time.Now()
}

var deliveryStatusOrder = map[string]int{
	"shipped": 0, "in_transit": 1, "out_for_delivery": 2, "delayed": 1, "delivered": 3,
}

func deliveryStatusRank(s string) int {
	if r, ok := deliveryStatusOrder[s]; ok {
		return r
	}
	return 0
}

// deliveryDescSimilar checks if two descriptions likely refer to the same product.
// Compares normalized words — if 50%+ of shorter description's words appear in longer, it's a match.
func deliveryDescSimilar(a, b string) bool {
	normalize := func(s string) []string {
		s = strings.ToLower(s)
		// Remove common noise
		for _, noise := range []string{",", ".", "-", "(", ")", "+", "&", "x2", "x3", "x4"} {
			s = strings.ReplaceAll(s, noise, " ")
		}
		words := strings.Fields(s)
		// Filter short/common words
		var result []string
		for _, w := range words {
			if len(w) > 2 {
				result = append(result, w)
			}
		}
		return result
	}

	wordsA := normalize(a)
	wordsB := normalize(b)
	if len(wordsA) == 0 || len(wordsB) == 0 {
		return false
	}

	// Use shorter as reference
	shorter, longer := wordsA, wordsB
	if len(wordsA) > len(wordsB) {
		shorter, longer = wordsB, wordsA
	}

	longerSet := make(map[string]bool)
	for _, w := range longer {
		longerSet[w] = true
	}

	matches := 0
	for _, w := range shorter {
		if longerSet[w] {
			matches++
		}
	}

	return float64(matches)/float64(len(shorter)) >= 0.5
}

func (s *DeliveryStore) Get(id string) (Delivery, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	for _, d := range s.Deliveries {
		if d.ID == id {
			return d, true
		}
	}
	return Delivery{}, false
}

func (s *DeliveryStore) Delete(id string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	for i, d := range s.Deliveries {
		if d.ID == id {
			s.Deliveries = append(s.Deliveries[:i], s.Deliveries[i+1:]...)
			s.save()
			return true
		}
	}
	return false
}

// Find by tracking number or description match
func (s *DeliveryStore) FindByTracking(tracking string) (Delivery, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	for _, d := range s.Deliveries {
		if d.TrackingNumber == tracking {
			return d, true
		}
	}
	return Delivery{}, false
}

// ============================================
// STATUS (key-value status indicators)
// ============================================

type StatusItem struct {
	Key       string    `json:"key"`
	Value     string    `json:"value"`
	Type      string    `json:"type,omitempty"` // info, success, warning, error
	UpdatedAt time.Time `json:"updated_at"`
}

type StatusStore struct {
	Items map[string]StatusItem `json:"items"`
	mu    sync.RWMutex
	path  string
}

func NewStatusStore(path string) (*StatusStore, error) {
	store := &StatusStore{path: path, Items: make(map[string]StatusItem)}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Items map[string]StatusItem `json:"items"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Items != nil {
		store.Items = wrapper.Items
		return store, nil
	}

	return store, store.save()
}

func (s *StatusStore) save() error {
	wrapper := struct {
		Items map[string]StatusItem `json:"items"`
	}{Items: s.Items}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *StatusStore) Set(key, value, itemType string) StatusItem {
	s.mu.Lock()
	defer s.mu.Unlock()

	item := StatusItem{
		Key:       key,
		Value:     value,
		Type:      itemType,
		UpdatedAt: time.Now(),
	}
	if item.Type == "" {
		item.Type = "info"
	}
	s.Items[key] = item
	s.save()
	return item
}

func (s *StatusStore) Get(key string) (StatusItem, bool) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	item, ok := s.Items[key]
	return item, ok
}

func (s *StatusStore) List() map[string]StatusItem {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.Items
}

func (s *StatusStore) Delete(key string) bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	if _, ok := s.Items[key]; ok {
		delete(s.Items, key)
		s.save()
		return true
	}
	return false
}

// ============================================
// EMAIL TRIAGE
// ============================================

type EmailTriageEntry struct {
	ID        string    `json:"id"`
	Account   string    `json:"account"`   // proton, johan
	From      string    `json:"from"`
	Subject   string    `json:"subject"`
	Action    string    `json:"action"`    // archived, deleted, flagged, kept
	Reason    string    `json:"reason,omitempty"`
	Timestamp time.Time `json:"timestamp"`
}

type EmailTriageStore struct {
	Entries []EmailTriageEntry `json:"entries"`
	mu      sync.RWMutex
	path    string
}

func NewEmailTriageStore(path string) (*EmailTriageStore, error) {
	store := &EmailTriageStore{path: path, Entries: []EmailTriageEntry{}}

	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, err
	}

	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return store, store.save()
		}
		return nil, err
	}

	var wrapper struct {
		Entries []EmailTriageEntry `json:"entries"`
	}
	if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.Entries != nil {
		store.Entries = wrapper.Entries
		return store, nil
	}

	return store, store.save()
}

func (s *EmailTriageStore) save() error {
	wrapper := struct {
		Entries []EmailTriageEntry `json:"entries"`
	}{Entries: s.Entries}
	data, err := json.MarshalIndent(wrapper, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(s.path, data, 0644)
}

func (s *EmailTriageStore) Add(entry EmailTriageEntry) (EmailTriageEntry, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	entry.ID = uuid.New().String()[:8]
	entry.Timestamp = time.Now()

	// Prepend (newest first)
	s.Entries = append([]EmailTriageEntry{entry}, s.Entries...)
	
	// Keep only last 500 entries
	if len(s.Entries) > 500 {
		s.Entries = s.Entries[:500]
	}
	
	return entry, s.save()
}

func (s *EmailTriageStore) List(account string, limit int) []EmailTriageEntry {
	s.mu.RLock()
	defer s.mu.RUnlock()

	if account == "" {
		if limit > 0 && limit < len(s.Entries) {
			return s.Entries[:limit]
		}
		return s.Entries
	}

	// Filter by account
	var filtered []EmailTriageEntry
	for _, e := range s.Entries {
		if e.Account == account {
			filtered = append(filtered, e)
			if limit > 0 && len(filtered) >= limit {
				break
			}
		}
	}
	return filtered
}

func (s *EmailTriageStore) Clear(account string) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	
	if account == "" {
		s.Entries = []EmailTriageEntry{}
	} else {
		var filtered []EmailTriageEntry
		for _, e := range s.Entries {
			if e.Account != account {
				filtered = append(filtered, e)
			}
		}
		s.Entries = filtered
	}
	return s.save()
}

// ============================================
// AGENTS (reads from OpenClaw config)
// ============================================

type Agent struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Emoji   string `json:"emoji"`
	URL     string `json:"url"`
	Default bool   `json:"default,omitempty"`
}

func getAgents(gatewayIP string, gatewayPort string, gatewayToken string) []Agent {
	// Read OpenClaw config
	home, _ := os.UserHomeDir()
	configPath := filepath.Join(home, ".openclaw", "openclaw.json")
	
	data, err := os.ReadFile(configPath)
	if err != nil {
		log.Printf("Failed to read openclaw config: %v", err)
		return []Agent{}
	}
	
	var config struct {
		Agents struct {
			List []struct {
				ID       string `json:"id"`
				Name     string `json:"name"`
				Identity struct {
					Name  string `json:"name"`
					Emoji string `json:"emoji"`
				} `json:"identity"`
			} `json:"list"`
		} `json:"agents"`
	}
	
	if err := json.Unmarshal(data, &config); err != nil {
		log.Printf("Failed to parse openclaw config: %v", err)
		return []Agent{}
	}
	
	var agents []Agent
	baseURL := "http://" + gatewayIP + ":" + gatewayPort
	
	for _, agentConfig := range config.Agents.List {
		agent := Agent{
			ID:    agentConfig.ID,
			Name:  agentConfig.Identity.Name,
			Emoji: agentConfig.Identity.Emoji,
		}
		
		// Set defaults if missing
		if agent.Name == "" {
			agent.Name = agentConfig.Name
		}
		if agent.Name == "" {
			agent.Name = agentConfig.ID
		}
		if agent.Emoji == "" {
			agent.Emoji = "🤖"
		}
		
		// Build URL - use /chat?session= (OpenClaw Control UI format)
		// Include token for auto-authentication
		if agentConfig.ID == "main" {
			agent.Default = true
			agent.URL = baseURL + "/chat?session=agent:main:main&token=" + gatewayToken
		} else {
			// Control UI reads session param on /chat route
			agent.URL = baseURL + "/chat?session=agent:" + agentConfig.ID + ":main&token=" + gatewayToken
		}
		
		agents = append(agents, agent)
	}
	
	// Sort: default first, then alphabetically
	sort.Slice(agents, func(i, j int) bool {
		if agents[i].Default != agents[j].Default {
			return agents[i].Default
		}
		return agents[i].ID < agents[j].ID
	})
	
	return agents
}

// ============================================
// MAIN
// ============================================

func main() {
	port := flag.String("port", "9200", "port to listen on")
	dir := flag.String("dir", ".", "directory to serve")
	gatewayIP := flag.String("gateway-ip", "192.168.1.16", "OpenClaw gateway IP")
	gatewayPort := flag.String("gateway-port", "18789", "OpenClaw gateway port")
	gatewayToken := flag.String("gateway-token", "2dee57cc3ce2947c27ce9e848d5c3e95cc452f25a1477462", "OpenClaw gateway auth token")
	flag.Parse()

	// Initialize stores
	taskStore, err := NewTaskStore(filepath.Join(*dir, "data", "tasks.json"))
	if err != nil {
		log.Fatalf("Failed to initialize task store: %v", err)
	}

	briefingStore, err := NewBriefingStore(filepath.Join(*dir, "data", "briefings.json"))
	if err != nil {
		log.Fatalf("Failed to initialize briefing store: %v", err)
	}

	newsStore, err := NewNewsStore(filepath.Join(*dir, "data", "news.json"))
	if err != nil {
		log.Fatalf("Failed to initialize news store: %v", err)
	}

	deliveryStore, err := NewDeliveryStore(filepath.Join(*dir, "data", "deliveries.json"))
	if err != nil {
		log.Fatalf("Failed to initialize delivery store: %v", err)
	}

	statusStore, err := NewStatusStore(filepath.Join(*dir, "data", "status.json"))
	if err != nil {
		log.Fatalf("Failed to initialize status store: %v", err)
	}

	emailTriageStore, err := NewEmailTriageStore(filepath.Join(*dir, "data", "email-triage.json"))
	if err != nil {
		log.Fatalf("Failed to initialize email triage store: %v", err)
	}

	mux := http.NewServeMux()

	// CORS middleware helper
	cors := func(w http.ResponseWriter) {
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
	}

	// ========== TASKS API ==========
	mux.HandleFunc("/api/tasks", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			json.NewEncoder(w).Encode(map[string]interface{}{
				"tasks": taskStore.List(),
			})

		case "POST":
			var task Task
			if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if task.Text == "" && task.Title == "" {
				http.Error(w, `{"error": "text or title required"}`, http.StatusBadRequest)
				return
			}
			newTask, err := taskStore.Add(task)
			if err != nil {
				http.Error(w, `{"error": "failed to save task"}`, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			json.NewEncoder(w).Encode(newTask)

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// Task by ID (DELETE, PATCH)
	mux.HandleFunc("/api/tasks/", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		id := strings.TrimPrefix(r.URL.Path, "/api/tasks/")
		if id == "" {
			http.Error(w, `{"error": "task id required"}`, http.StatusBadRequest)
			return
		}

		switch r.Method {
		case "DELETE":
			if taskStore.Delete(id) {
				json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id})
			} else {
				http.Error(w, `{"error": "task not found"}`, http.StatusNotFound)
			}

		case "PATCH", "PUT":
			var updates Task
			if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if updated, ok := taskStore.Update(id, updates); ok {
				json.NewEncoder(w).Encode(updated)
			} else {
				http.Error(w, `{"error": "task not found"}`, http.StatusNotFound)
			}

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== NEWS API ==========
	mux.HandleFunc("/api/news", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			json.NewEncoder(w).Encode(map[string]interface{}{
				"items": newsStore.List(20),
			})

		case "POST":
			var item NewsItem
			if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if item.Title == "" {
				http.Error(w, `{"error": "title required"}`, http.StatusBadRequest)
				return
			}
			newItem, err := newsStore.Add(item)
			if err != nil {
				http.Error(w, `{"error": "failed to save news"}`, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			json.NewEncoder(w).Encode(newItem)

		case "DELETE":
			// DELETE /api/news clears all news
			if err := newsStore.Clear(); err != nil {
				http.Error(w, `{"error": "failed to clear news"}`, http.StatusInternalServerError)
				return
			}
			json.NewEncoder(w).Encode(map[string]string{"status": "cleared"})

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// News by ID
	mux.HandleFunc("/api/news/", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		id := strings.TrimPrefix(r.URL.Path, "/api/news/")
		if id == "" {
			http.Error(w, `{"error": "news id required"}`, http.StatusBadRequest)
			return
		}

		switch r.Method {
		case "DELETE":
			if newsStore.Delete(id) {
				json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id})
			} else {
				http.Error(w, `{"error": "news item not found"}`, http.StatusNotFound)
			}

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== BRIEFINGS API ==========
	mux.HandleFunc("/api/briefings", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			limit := 30 // default
			json.NewEncoder(w).Encode(map[string]interface{}{
				"briefings": briefingStore.List(limit),
			})

		case "POST":
			var b Briefing
			if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			newBriefing, err := briefingStore.Add(b)
			if err != nil {
				http.Error(w, `{"error": "failed to save briefing"}`, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			json.NewEncoder(w).Encode(newBriefing)

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// Briefing by ID
	mux.HandleFunc("/api/briefings/", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		id := strings.TrimPrefix(r.URL.Path, "/api/briefings/")
		if id == "" {
			http.Error(w, `{"error": "briefing id required"}`, http.StatusBadRequest)
			return
		}

		switch r.Method {
		case "GET":
			if b, ok := briefingStore.Get(id); ok {
				json.NewEncoder(w).Encode(b)
			} else {
				http.Error(w, `{"error": "briefing not found"}`, http.StatusNotFound)
			}

		case "DELETE":
			if briefingStore.Delete(id) {
				json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id})
			} else {
				http.Error(w, `{"error": "briefing not found"}`, http.StatusNotFound)
			}

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== DELIVERIES API ==========
	mux.HandleFunc("/api/deliveries", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			// ?all=true returns everything including delivered
			if r.URL.Query().Get("all") == "true" {
				json.NewEncoder(w).Encode(map[string]interface{}{
					"deliveries": deliveryStore.ListAll(),
				})
			} else {
				json.NewEncoder(w).Encode(map[string]interface{}{
					"deliveries": deliveryStore.List(),
				})
			}

		case "POST":
			var d Delivery
			if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if d.Description == "" {
				http.Error(w, `{"error": "description required"}`, http.StatusBadRequest)
				return
			}
			newDelivery, err := deliveryStore.Add(d)
			if err != nil {
				http.Error(w, `{"error": "failed to save delivery"}`, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			json.NewEncoder(w).Encode(newDelivery)

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// Delivery upsert — match by tracking_number or description, update or create
	mux.HandleFunc("/api/deliveries/upsert", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "PUT" && r.Method != "POST" {
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
			return
		}

		var d Delivery
		if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
			http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
			return
		}
		if d.Description == "" {
			http.Error(w, `{"error": "description required"}`, http.StatusBadRequest)
			return
		}

		result, wasUpdate, err := deliveryStore.Upsert(d)
		if err != nil {
			http.Error(w, `{"error": "failed to save"}`, http.StatusInternalServerError)
			return
		}

		status := http.StatusCreated
		action := "created"
		if wasUpdate {
			status = http.StatusOK
			action = "updated"
		}
		w.WriteHeader(status)
		json.NewEncoder(w).Encode(map[string]interface{}{
			"delivery": result,
			"action":   action,
		})
	})

	// Delivery by ID
	mux.HandleFunc("/api/deliveries/", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		id := strings.TrimPrefix(r.URL.Path, "/api/deliveries/")
		if id == "" {
			http.Error(w, `{"error": "delivery id required"}`, http.StatusBadRequest)
			return
		}

		switch r.Method {
		case "GET":
			if d, ok := deliveryStore.Get(id); ok {
				json.NewEncoder(w).Encode(d)
			} else {
				http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound)
			}

		case "PATCH", "PUT":
			var updates Delivery
			if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if updated, ok := deliveryStore.Update(id, updates); ok {
				json.NewEncoder(w).Encode(updated)
			} else {
				http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound)
			}

		case "DELETE":
			if deliveryStore.Delete(id) {
				json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "id": id})
			} else {
				http.Error(w, `{"error": "delivery not found"}`, http.StatusNotFound)
			}

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== STATUS API ==========
	mux.HandleFunc("/api/status", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			json.NewEncoder(w).Encode(map[string]interface{}{
				"status": statusStore.List(),
			})

		case "POST", "PUT":
			var item struct {
				Key   string `json:"key"`
				Value string `json:"value"`
				Type  string `json:"type"`
			}
			if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if item.Key == "" || item.Value == "" {
				http.Error(w, `{"error": "key and value required"}`, http.StatusBadRequest)
				return
			}
			result := statusStore.Set(item.Key, item.Value, item.Type)
			json.NewEncoder(w).Encode(result)

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// Status by key
	mux.HandleFunc("/api/status/", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		key := strings.TrimPrefix(r.URL.Path, "/api/status/")
		if key == "" {
			http.Error(w, `{"error": "status key required"}`, http.StatusBadRequest)
			return
		}

		switch r.Method {
		case "GET":
			if item, ok := statusStore.Get(key); ok {
				json.NewEncoder(w).Encode(item)
			} else {
				http.Error(w, `{"error": "status not found"}`, http.StatusNotFound)
			}

		case "DELETE":
			if statusStore.Delete(key) {
				json.NewEncoder(w).Encode(map[string]string{"status": "deleted", "key": key})
			} else {
				http.Error(w, `{"error": "status not found"}`, http.StatusNotFound)
			}

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== EMAIL TRIAGE API ==========
	mux.HandleFunc("/api/email-triage", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		switch r.Method {
		case "GET":
			account := r.URL.Query().Get("account")
			limit := 100
			if l := r.URL.Query().Get("limit"); l != "" {
				if n, err := json.Number(l).Int64(); err == nil {
					limit = int(n)
				}
			}
			json.NewEncoder(w).Encode(map[string]interface{}{
				"entries": emailTriageStore.List(account, limit),
			})

		case "POST":
			var entry EmailTriageEntry
			if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
				http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
				return
			}
			if entry.Account == "" || entry.Action == "" {
				http.Error(w, `{"error": "account and action required"}`, http.StatusBadRequest)
				return
			}
			newEntry, err := emailTriageStore.Add(entry)
			if err != nil {
				http.Error(w, `{"error": "failed to save entry"}`, http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusCreated)
			json.NewEncoder(w).Encode(newEntry)

		case "DELETE":
			account := r.URL.Query().Get("account")
			if err := emailTriageStore.Clear(account); err != nil {
				http.Error(w, `{"error": "failed to clear"}`, http.StatusInternalServerError)
				return
			}
			json.NewEncoder(w).Encode(map[string]string{"status": "cleared", "account": account})

		default:
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// ========== CLAUDE USAGE API ==========
	claudeUsageFile := "/home/johan/clawd/memory/claude-usage.json"
	claudeHistoryFile := filepath.Join(*dir, "data", "claude-usage-history.json")

	mux.HandleFunc("/api/claude-usage", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "GET" {
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
			return
		}

		// Read current usage
		result := make(map[string]interface{})
		if data, err := os.ReadFile(claudeUsageFile); err == nil {
			json.Unmarshal(data, &result)
		}

		// Read history
		var history []map[string]interface{}
		if data, err := os.ReadFile(claudeHistoryFile); err == nil {
			json.Unmarshal(data, &history)
		}
		result["history"] = history

		json.NewEncoder(w).Encode(result)
	})

	mux.HandleFunc("/api/claude-usage/record", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}
		if r.Method != "POST" {
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
			return
		}

		var entry map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
			http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
			return
		}

		// Read existing history
		var history []map[string]interface{}
		if data, err := os.ReadFile(claudeHistoryFile); err == nil {
			json.Unmarshal(data, &history)
		}

		// Add timestamp if missing
		if _, ok := entry["timestamp"]; !ok {
			entry["timestamp"] = time.Now().UTC().Format(time.RFC3339)
		}

		history = append(history, entry)

		// Keep last 2000 entries
		if len(history) > 2000 {
			history = history[len(history)-2000:]
		}

		data, _ := json.MarshalIndent(history, "", "  ")
		os.WriteFile(claudeHistoryFile, data, 0644)

		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(map[string]string{"status": "recorded"})
	})

	// ========== AGENTS API ==========
	mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) {
		cors(w)
		if r.Method == "OPTIONS" {
			return
		}

		if r.Method == "GET" {
			agents := getAgents(*gatewayIP, *gatewayPort, *gatewayToken)
			json.NewEncoder(w).Encode(map[string]interface{}{
				"agents":     agents,
				"gateway_ip": *gatewayIP,
				"gateway_port": *gatewayPort,
			})
		} else {
			http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
		}
	})

	// Static files (fallback)
	fs := http.FileServer(http.Dir(*dir))
	mux.Handle("/", fs)

	server := &http.Server{
		Addr:    "0.0.0.0:" + *port,
		Handler: mux,
	}

	// Graceful shutdown
	go func() {
		sigChan := make(chan os.Signal, 1)
		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
		<-sigChan
		log.Println("Shutting down gracefully...")
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		server.Shutdown(ctx)
	}()

	log.Printf("James Dashboard serving %s on http://0.0.0.0:%s\n", *dir, *port)
	log.Printf("API endpoints:\n")
	log.Printf("  Tasks:\n")
	log.Printf("    GET    /api/tasks        - list all tasks\n")
	log.Printf("    POST   /api/tasks        - add task {title, priority, status, owner, notes}\n")
	log.Printf("    PATCH  /api/tasks/:id    - update task\n")
	log.Printf("    DELETE /api/tasks/:id    - remove task\n")
	log.Printf("  News:\n")
	log.Printf("    GET    /api/news         - list news (newest first, max 20)\n")
	log.Printf("    POST   /api/news         - add news {title, body, type, source, url}\n")
	log.Printf("    DELETE /api/news         - clear all news\n")
	log.Printf("    DELETE /api/news/:id     - remove news item\n")
	log.Printf("  Briefings:\n")
	log.Printf("    GET    /api/briefings    - list briefings (newest first)\n")
	log.Printf("    POST   /api/briefings    - add briefing {title, date, weather, markets, news, summary}\n")
	log.Printf("    GET    /api/briefings/:id - get single briefing\n")
	log.Printf("    DELETE /api/briefings/:id - remove briefing\n")
	log.Printf("  Deliveries:\n")
	log.Printf("    GET    /api/deliveries      - list active deliveries (excludes delivered)\n")
	log.Printf("    GET    /api/deliveries?all=true - list all deliveries\n")
	log.Printf("    POST   /api/deliveries      - add delivery {description, carrier, retailer, tracking_number, expected_date, status}\n")
	log.Printf("    GET    /api/deliveries/:id  - get single delivery\n")
	log.Printf("    PATCH  /api/deliveries/:id  - update delivery\n")
	log.Printf("    DELETE /api/deliveries/:id  - remove delivery\n")
	log.Printf("  Email Triage:\n")
	log.Printf("    GET    /api/email-triage            - list email triage log (newest first)\n")
	log.Printf("    GET    /api/email-triage?account=X  - filter by account (proton, johan)\n")
	log.Printf("    POST   /api/email-triage            - add entry {account, from, subject, action, reason}\n")
	log.Printf("    DELETE /api/email-triage            - clear all entries\n")
	log.Printf("    DELETE /api/email-triage?account=X  - clear entries for account\n")
	log.Printf("  Agents:\n")
	log.Printf("    GET    /api/agents       - list OpenClaw agents (reads from config)\n")
	log.Printf("  Gateway: http://%s:%s\n", *gatewayIP, *gatewayPort)

	if err := server.ListenAndServe(); err != http.ErrServerClosed {
		log.Fatal(err)
	}
}
