package localcache

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/databricks/cli/libs/log"
)

const userRW = 0o600
const ownerRWXworldRX = 0o755

func NewLocalCache[T any](dir, name string, validity time.Duration) LocalCache[T] {
	return LocalCache[T]{
		dir:      dir,
		name:     name,
		validity: validity,
	}
}

type LocalCache[T any] struct {
	name     string
	dir      string
	validity time.Duration
	zero     T
}

func (r *LocalCache[T]) Load(ctx context.Context, refresh func() (T, error)) (T, error) {
	cached, err := r.loadCache()
	if errors.Is(err, fs.ErrNotExist) {
		return r.refreshCache(ctx, refresh, r.zero)
	} else if err != nil {
		return r.zero, err
	} else if time.Since(cached.Refreshed) > r.validity {
		return r.refreshCache(ctx, refresh, cached.Data)
	}
	return cached.Data, nil
}

type cached[T any] struct {
	// we don't use mtime of the file because it's easier to
	// for testdata used in the unit tests to be somewhere far
	// in the future and don't bother about switching the mtime bit.
	Refreshed time.Time `json:"refreshed_at"`
	Data      T         `json:"data"`
}

func (r *LocalCache[T]) refreshCache(ctx context.Context, refresh func() (T, error), offlineVal T) (T, error) {
	data, err := refresh()
	var urlError *url.Error
	if errors.As(err, &urlError) {
		log.Warnf(ctx, "System offline. Cannot refresh cache: %s", urlError)
		return offlineVal, nil
	}
	if err != nil {
		return r.zero, fmt.Errorf("refresh: %w", err)
	}
	return r.writeCache(ctx, data)
}

func (r *LocalCache[T]) writeCache(ctx context.Context, data T) (T, error) {
	cached := &cached[T]{time.Now(), data}
	raw, err := json.MarshalIndent(cached, "", "  ")
	if err != nil {
		return r.zero, fmt.Errorf("json marshal: %w", err)
	}
	cacheFile := r.FileName()
	err = os.WriteFile(cacheFile, raw, userRW)
	if errors.Is(err, fs.ErrNotExist) {
		cacheDir := filepath.Dir(cacheFile)
		err := os.MkdirAll(cacheDir, ownerRWXworldRX)
		if err != nil {
			return r.zero, fmt.Errorf("create %s: %w", cacheDir, err)
		}
		err = os.WriteFile(cacheFile, raw, userRW)
		if err != nil {
			return r.zero, fmt.Errorf("retry save cache: %w", err)
		}
		return data, nil
	} else if err != nil {
		return r.zero, fmt.Errorf("save cache: %w", err)
	}
	return data, nil
}

func (r *LocalCache[T]) FileName() string {
	return filepath.Join(r.dir, fmt.Sprintf("%s.json", r.name))
}

func (r *LocalCache[T]) loadCache() (*cached[T], error) {
	jsonFile := r.FileName()
	raw, err := os.ReadFile(r.FileName())
	if err != nil {
		return nil, fmt.Errorf("read %s: %w", jsonFile, err)
	}
	var v cached[T]
	err = json.Unmarshal(raw, &v)
	if err != nil {
		return nil, fmt.Errorf("parse %s: %w", jsonFile, err)
	}
	return &v, nil
}
