package main

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

const (
	locationCacheFile = "location.json"
	forecastCacheFile = "forecast.json"
)

type Cache struct {
	dir string
}

type LocationCache struct {
	Query     string    `json:"query"`
	Location  Location  `json:"location"`
	UpdatedAt time.Time `json:"updated_at"`
}

type ForecastCache struct {
	Location  Location       `json:"location"`
	Unit      string         `json:"unit"`
	Period    ForecastPeriod `json:"period"`
	FetchedAt time.Time      `json:"fetched_at"`
}

func NewCache(dir string) *Cache {
	return &Cache{dir: dir}
}

func (c *Cache) SaveLocation(query string, loc Location) error {
	if err := c.ensureDir(); err != nil {
		return err
	}
	payload := LocationCache{
		Query:     query,
		Location:  loc,
		UpdatedAt: time.Now().UTC(),
	}
	return writeJSON(filepath.Join(c.dir, locationCacheFile), payload)
}

func (c *Cache) LoadLocation() (Location, bool, error) {
	path := filepath.Join(c.dir, locationCacheFile)
	var payload LocationCache
	if err := readJSON(path, &payload); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return Location{}, false, nil
		}
		return Location{}, false, err
	}
	return payload.Location, true, nil
}

func (c *Cache) SaveForecast(loc Location, unit string, period ForecastPeriod) error {
	if err := c.ensureDir(); err != nil {
		return err
	}
	payload := ForecastCache{
		Location:  loc,
		Unit:      unit,
		Period:    period,
		FetchedAt: time.Now().UTC(),
	}
	return writeJSON(filepath.Join(c.dir, forecastCacheFile), payload)
}

func (c *Cache) LoadForecast(loc Location, unit string, ttl time.Duration) (ForecastPeriod, bool, error) {
	path := filepath.Join(c.dir, forecastCacheFile)
	var payload ForecastCache
	if err := readJSON(path, &payload); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ForecastPeriod{}, false, nil
		}
		return ForecastPeriod{}, false, err
	}

	if payload.Unit != unit {
		return ForecastPeriod{}, false, nil
	}
	if !sameLocation(payload.Location, loc) {
		return ForecastPeriod{}, false, nil
	}
	if time.Since(payload.FetchedAt) > ttl {
		return ForecastPeriod{}, false, nil
	}

	return payload.Period, true, nil
}

func (c *Cache) ensureDir() error {
	if c.dir == "" {
		return fmt.Errorf("cache directory is empty")
	}
	return os.MkdirAll(c.dir, 0o755)
}

func (c *Cache) Clear() error {
	if c.dir == "" {
		return fmt.Errorf("cache directory is empty")
	}
	if err := os.Remove(filepath.Join(c.dir, locationCacheFile)); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	if err := os.Remove(filepath.Join(c.dir, forecastCacheFile)); err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}

func readJSON(path string, target any) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	return json.Unmarshal(data, target)
}

func writeJSON(path string, value any) error {
	data, err := json.MarshalIndent(value, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

func sameLocation(a, b Location) bool {
	return a.Lat == b.Lat && a.Lon == b.Lon
}

func cacheRoot() (string, error) {
	if override := os.Getenv("WEATHER_CACHE_DIR"); override != "" {
		return override, nil
	}
	if base := os.Getenv("XDG_CACHE_HOME"); base != "" {
		return filepath.Join(base, "weather"), nil
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".cache", "weather"), nil
}
