package storage

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"time"
)

type ObjectMeta struct {
	Key         string    `json:"key"`
	URL         string    `json:"url"`
	Filename    string    `json:"filename"`
	ContentType string    `json:"content_type"`
	Size        int64     `json:"size"`
	CreatedAt   time.Time `json:"created_at"`
}

type Store interface {
	Put(ctx context.Context, tenantID int64, filename, contentType string, r io.Reader, size int64) (*ObjectMeta, error)
	Open(ctx context.Context, key string) (io.ReadCloser, *ObjectMeta, error)
}

type LocalStore struct {
	Root      string
	PublicBase string // e.g. http://127.0.0.1:8180/api/v1/storage
}

func NewLocalStore(root, publicBase string) (*LocalStore, error) {
	if root == "" {
		root = "./data/uploads"
	}
	if err := os.MkdirAll(root, 0o755); err != nil {
		return nil, err
	}
	return &LocalStore{Root: root, PublicBase: strings.TrimRight(publicBase, "/")}, nil
}

func (s *LocalStore) Put(_ context.Context, tenantID int64, filename, contentType string, r io.Reader, size int64) (*ObjectMeta, error) {
	ext := filepath.Ext(filename)
	if ext == "" {
		ext = guessExt(contentType)
	}
	id := make([]byte, 8)
	_, _ = rand.Read(id)
	key := fmt.Sprintf("t%d/%s/%s%s", tenantID, time.Now().UTC().Format("20060102"), hex.EncodeToString(id), ext)
	full := filepath.Join(s.Root, filepath.FromSlash(key))
	if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
		return nil, err
	}
	f, err := os.Create(full)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	n, err := io.Copy(f, r)
	if err != nil {
		return nil, err
	}
	if size <= 0 {
		size = n
	}
	meta := &ObjectMeta{
		Key: key, Filename: filename, ContentType: contentType, Size: size, CreatedAt: time.Now().UTC(),
		URL: s.PublicBase + "/" + key,
	}
	// sidecar meta
	_ = os.WriteFile(full+".meta", []byte(fmt.Sprintf("%s\n%s\n%d\n%s", filename, contentType, size, meta.CreatedAt.Format(time.RFC3339))), 0o644)
	return meta, nil
}

func (s *LocalStore) Open(_ context.Context, key string) (io.ReadCloser, *ObjectMeta, error) {
	key = strings.TrimPrefix(key, "/")
	if strings.Contains(key, "..") {
		return nil, nil, fmt.Errorf("invalid key")
	}
	full := filepath.Join(s.Root, filepath.FromSlash(key))
	f, err := os.Open(full)
	if err != nil {
		return nil, nil, err
	}
	st, _ := f.Stat()
	meta := &ObjectMeta{Key: key, Size: st.Size(), URL: s.PublicBase + "/" + key, CreatedAt: st.ModTime().UTC()}
	if b, err := os.ReadFile(full + ".meta"); err == nil {
		parts := strings.Split(string(b), "\n")
		if len(parts) >= 2 {
			meta.Filename = parts[0]
			meta.ContentType = parts[1]
		}
	}
	return f, meta, nil
}

func guessExt(ct string) string {
	switch ct {
	case "image/png":
		return ".png"
	case "image/jpeg":
		return ".jpg"
	case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
		return ".xlsx"
	case "text/csv":
		return ".csv"
	default:
		return ".bin"
	}
}
