package storage

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"path"
	"strings"
	"time"

	minio "github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"

	"github.com/libnovel/backend/internal/config"
)

// minioClient wraps the official minio-go client with bucket names.
type minioClient struct {
	client             *minio.Client // internal — all read/write operations
	pubClient          *minio.Client // presign-only — initialised against the public endpoint
	bucketChapters     string
	bucketAudio        string
	bucketAvatars      string
	bucketBrowse       string
	bucketTranslations string
}

func newMinioClient(cfg config.MinIO) (*minioClient, error) {
	creds := credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, "")

	internal, err := minio.New(cfg.Endpoint, &minio.Options{
		Creds:  creds,
		Secure: cfg.UseSSL,
	})
	if err != nil {
		return nil, fmt.Errorf("minio: init internal client: %w", err)
	}

	// Presigned URLs must be signed with the hostname the browser will use
	// (PUBLIC_MINIO_PUBLIC_URL), because AWS Signature V4 includes the Host
	// header in the canonical request — a URL signed against "minio:9000" will
	// return SignatureDoesNotMatch when the browser fetches it from
	// "localhost:9000".
	//
	// However, minio-go normally makes a live BucketLocation HTTP call before
	// signing, which would fail from inside the container when the public
	// endpoint is externally-facing (e.g. "localhost:9000" is unreachable from
	// within Docker).  We prevent this by:
	//   1. Setting Region: "us-east-1" — minio-go skips getBucketLocation when
	//      the region is already known (bucket-cache.go:49).
	//   2. Setting BucketLookup: BucketLookupPath — forces path-style URLs
	//      (e.g. host/bucket/key), matching MinIO's default behaviour and
	//      avoiding any virtual-host DNS probing.
	//
	// When no public endpoint is configured (or it equals the internal one),
	// fall back to the internal client so presigning still works.
	publicEndpoint := cfg.PublicEndpoint
	if u, err2 := url.Parse(publicEndpoint); err2 == nil && u.Host != "" {
		publicEndpoint = u.Host // strip scheme so minio.New is happy
	}
	pubUseSSL := cfg.PublicUseSSL
	if publicEndpoint == "" || publicEndpoint == cfg.Endpoint {
		publicEndpoint = cfg.Endpoint
		pubUseSSL = cfg.UseSSL
	}
	pub, err := minio.New(publicEndpoint, &minio.Options{
		Creds:        creds,
		Secure:       pubUseSSL,
		Region:       "us-east-1", // skip live BucketLocation preflight
		BucketLookup: minio.BucketLookupPath,
	})
	if err != nil {
		return nil, fmt.Errorf("minio: init public client: %w", err)
	}

	return &minioClient{
		client:             internal,
		pubClient:          pub,
		bucketChapters:     cfg.BucketChapters,
		bucketAudio:        cfg.BucketAudio,
		bucketAvatars:      cfg.BucketAvatars,
		bucketBrowse:       cfg.BucketBrowse,
		bucketTranslations: cfg.BucketTranslations,
	}, nil
}

// ensureBuckets creates all required buckets if they don't already exist.
func (m *minioClient) ensureBuckets(ctx context.Context) error {
	for _, bucket := range []string{m.bucketChapters, m.bucketAudio, m.bucketAvatars, m.bucketBrowse, m.bucketTranslations} {
		exists, err := m.client.BucketExists(ctx, bucket)
		if err != nil {
			return fmt.Errorf("minio: check bucket %q: %w", bucket, err)
		}
		if !exists {
			if err := m.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{}); err != nil {
				return fmt.Errorf("minio: create bucket %q: %w", bucket, err)
			}
		}
	}
	return nil
}

// ── Key helpers ───────────────────────────────────────────────────────────────

// ChapterObjectKey returns the MinIO object key for a chapter markdown file.
// Format: {slug}/chapter-{n:06d}.md
func ChapterObjectKey(slug string, n int) string {
	return fmt.Sprintf("%s/chapter-%06d.md", slug, n)
}

// AudioObjectKeyExt returns the MinIO object key for a cached audio file
// with a custom extension (e.g. "mp3" or "wav").
// Format: {slug}/{n}/{voice}.{ext}
func AudioObjectKeyExt(slug string, n int, voice, ext string) string {
	return fmt.Sprintf("%s/%d/%s.%s", slug, n, voice, ext)
}

// AudioObjectKey returns the MinIO object key for a cached MP3 audio file.
// Format: {slug}/{n}/{voice}.mp3
func AudioObjectKey(slug string, n int, voice string) string {
	return AudioObjectKeyExt(slug, n, voice, "mp3")
}

// AvatarObjectKey returns the MinIO object key for a user avatar image.
// Format: {userID}/{ext}.{ext}
func AvatarObjectKey(userID, ext string) string {
	return fmt.Sprintf("%s/%s.%s", userID, ext, ext)
}

// CoverObjectKey returns the MinIO object key for a book cover image.
// Format: covers/{slug}.jpg
func CoverObjectKey(slug string) string {
	return fmt.Sprintf("covers/%s.jpg", slug)
}

// ChapterImageObjectKey returns the MinIO object key for a chapter illustration.
// Format: chapter-images/{slug}/{n:06d}.jpg
func ChapterImageObjectKey(slug string, n int) string {
	return fmt.Sprintf("chapter-images/%s/%06d.jpg", slug, n)
}

// TranslationObjectKey returns the MinIO object key for a translated chapter.
// Format: {lang}/{slug}/{n:06d}.md
func TranslationObjectKey(lang, slug string, n int) string {
	return fmt.Sprintf("%s/%s/%06d.md", lang, slug, n)
}

// chapterNumberFromKey extracts the chapter number from a MinIO object key.
// e.g. "my-book/chapter-000042.md" → 42
func chapterNumberFromKey(key string) int {
	base := path.Base(key)
	base = strings.TrimPrefix(base, "chapter-")
	base = strings.TrimSuffix(base, ".md")
	var n int
	fmt.Sscanf(base, "%d", &n)
	return n
}

// ── Object operations ─────────────────────────────────────────────────────────

func (m *minioClient) putObject(ctx context.Context, bucket, key, contentType string, data []byte) error {
	_, err := m.client.PutObject(ctx, bucket, key,
		strings.NewReader(string(data)),
		int64(len(data)),
		minio.PutObjectOptions{ContentType: contentType},
	)
	return err
}

// putObjectStream uploads from r with known size (or -1 for multipart).
func (m *minioClient) putObjectStream(ctx context.Context, bucket, key, contentType string, r io.Reader, size int64) error {
	_, err := m.client.PutObject(ctx, bucket, key, r, size,
		minio.PutObjectOptions{ContentType: contentType},
	)
	return err
}

func (m *minioClient) getObject(ctx context.Context, bucket, key string) ([]byte, error) {
	obj, err := m.client.GetObject(ctx, bucket, key, minio.GetObjectOptions{})
	if err != nil {
		return nil, err
	}
	defer obj.Close()
	return io.ReadAll(obj)
}

func (m *minioClient) objectExists(ctx context.Context, bucket, key string) bool {
	_, err := m.client.StatObject(ctx, bucket, key, minio.StatObjectOptions{})
	return err == nil
}

func (m *minioClient) presignGet(ctx context.Context, bucket, key string, expires time.Duration) (string, error) {
	u, err := m.pubClient.PresignedGetObject(ctx, bucket, key, expires, nil)
	if err != nil {
		return "", fmt.Errorf("minio presign %s/%s: %w", bucket, key, err)
	}
	return u.String(), nil
}

func (m *minioClient) presignPut(ctx context.Context, bucket, key string, expires time.Duration) (string, error) {
	u, err := m.pubClient.PresignedPutObject(ctx, bucket, key, expires)
	if err != nil {
		return "", fmt.Errorf("minio presign PUT %s/%s: %w", bucket, key, err)
	}
	return u.String(), nil
}

func (m *minioClient) deleteObjects(ctx context.Context, bucket, prefix string) error {
	objCh := m.client.ListObjects(ctx, bucket, minio.ListObjectsOptions{Prefix: prefix})
	for obj := range objCh {
		if obj.Err != nil {
			return obj.Err
		}
		if err := m.client.RemoveObject(ctx, bucket, obj.Key, minio.RemoveObjectOptions{}); err != nil {
			return err
		}
	}
	return nil
}

func (m *minioClient) listObjectKeys(ctx context.Context, bucket, prefix string) ([]string, error) {
	var keys []string
	for obj := range m.client.ListObjects(ctx, bucket, minio.ListObjectsOptions{Prefix: prefix}) {
		if obj.Err != nil {
			return nil, obj.Err
		}
		keys = append(keys, obj.Key)
	}
	return keys, nil
}

// ── Cover operations ──────────────────────────────────────────────────────────

// putCover stores a raw cover image in the browse bucket under covers/{slug}.jpg.
func (m *minioClient) putCover(ctx context.Context, key, contentType string, data []byte) error {
	return m.putObject(ctx, m.bucketBrowse, key, contentType, data)
}

// getCover retrieves a cover image. Returns (nil, "", false, nil) when the
// object does not exist.
func (m *minioClient) getCover(ctx context.Context, key string) ([]byte, bool, error) {
	if !m.objectExists(ctx, m.bucketBrowse, key) {
		return nil, false, nil
	}
	data, err := m.getObject(ctx, m.bucketBrowse, key)
	if err != nil {
		return nil, false, err
	}
	return data, true, nil
}

// coverExists returns true when the cover image object exists.
func (m *minioClient) coverExists(ctx context.Context, key string) bool {
	return m.objectExists(ctx, m.bucketBrowse, key)
}

// coverContentType inspects the first bytes of data to determine if it is
// a JPEG or PNG image. Falls back to "image/jpeg".
func coverContentType(data []byte) string {
	if len(data) >= 4 {
		// PNG magic: 0x89 0x50 0x4E 0x47
		if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
			return "image/png"
		}
		// WebP: starts with "RIFF" at 0..3 and "WEBP" at 8..11
		if len(data) >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' &&
			data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P' {
			return "image/webp"
		}
	}
	return "image/jpeg"
}

// ── Chapter image operations ───────────────────────────────────────────────────

// putChapterImage stores a chapter illustration in the browse bucket.
func (m *minioClient) putChapterImage(ctx context.Context, key, contentType string, data []byte) error {
	return m.putObject(ctx, m.bucketBrowse, key, contentType, data)
}

// getChapterImage retrieves a chapter illustration. Returns (nil, false, nil)
// when the object does not exist.
func (m *minioClient) getChapterImage(ctx context.Context, key string) ([]byte, bool, error) {
	if !m.objectExists(ctx, m.bucketBrowse, key) {
		return nil, false, nil
	}
	data, err := m.getObject(ctx, m.bucketBrowse, key)
	if err != nil {
		return nil, false, err
	}
	return data, true, nil
}

// chapterImageExists returns true when the chapter image object exists.
func (m *minioClient) chapterImageExists(ctx context.Context, key string) bool {
	return m.objectExists(ctx, m.bucketBrowse, key)
}
