package main

import (
	"context"
	cryptorand "crypto/rand"
	"encoding/binary"
	"errors"
	"fmt"
	"math/rand/v2"
	"net/url"
	"sync"
	"time"
)

// Aturan domain layanan pemendek URL.
const (
	// panjangKode adalah jumlah karakter short code produksi.
	panjangKode = 7
	// maxPercobaanKode membatasi retry pembuatan kode saat bentrok.
	maxPercobaanKode = 5
	// maxPanjangURL membatasi URL asli yang diterima.
	maxPanjangURL = 2048
)

// ValidationError menjelaskan satu field masukan yang melanggar
// aturan domain.
type ValidationError struct {
	Field   string
	Message string
}

// Error memenuhi kontrak interface error.
func (e ValidationError) Error() string {
	return fmt.Sprintf("field %s: %s", e.Field, e.Message)
}

// Clock memisahkan sumber waktu dari logika bisnis. Produksi memakai
// RealClock; demo dan unit test memakai FixedClock sehingga nilai
// created_at selalu deterministik.
type Clock interface {
	Now() time.Time
}

// RealClock mengembalikan waktu sistem dalam zona UTC.
type RealClock struct{}

// Now memenuhi interface Clock.
func (RealClock) Now() time.Time { return time.Now().UTC() }

// FixedClock selalu mengembalikan Waktu yang sama.
type FixedClock struct {
	Waktu time.Time
}

// Now memenuhi interface Clock.
func (c FixedClock) Now() time.Time { return c.Waktu }

// CodeGenerator memproduksi kandidat short code baru. Produksi
// memakai Base62Generator; test dan demo memakai generator stub
// yang mengembalikan urutan tetap.
type CodeGenerator interface {
	NewCode() (string, error)
}

// ShortenService menampung aturan pemendekan URL: validasi masukan,
// pembuatan kode unik dengan retry, penghitungan klik, dan
// statistik — seluruh akses penyimpanan didelegasikan ke
// URLRepository yang diinjeksi.
type ShortenService struct {
	repo  URLRepository
	gen   CodeGenerator
	clock Clock
}

// NewShortenService membangun service dengan repository, generator
// kode, dan clock yang diinjeksi pemanggil.
func NewShortenService(repo URLRepository, gen CodeGenerator, clock Clock) *ShortenService {
	return &ShortenService{repo: repo, gen: gen, clock: clock}
}

// Shorten memvalidasi URL, lalu mencoba menyimpan pemetaan dengan
// kode dari generator. Bila kode bentrok (ErrCodeExists), kode baru
// dibuat lagi sampai maxPercobaanKode kali.
func (s *ShortenService) Shorten(ctx context.Context, rawURL string) (URLRecord, error) {
	if err := ValidasiURL(rawURL); err != nil {
		return URLRecord{}, err
	}

	for percobaan := 1; percobaan <= maxPercobaanKode; percobaan++ {
		code, err := s.gen.NewCode()
		if err != nil {
			return URLRecord{}, fmt.Errorf("membuat short code: %w", err)
		}
		rec := URLRecord{
			Code:        code,
			OriginalURL: rawURL,
			CreatedAt:   s.clock.Now(),
		}

		err = s.repo.Save(ctx, rec)
		switch {
		case err == nil:
			return rec, nil
		case errors.Is(err, ErrCodeExists):
			continue // kode sudah dipakai: coba kode berikutnya
		default:
			return URLRecord{}, err
		}
	}
	return URLRecord{}, fmt.Errorf("kode unik tidak didapat setelah %d percobaan: %w", maxPercobaanKode, ErrCodeExists)
}

// Redirect menaikkan jumlah klik satu kali lalu mengembalikan URL
// asli yang menjadi tujuan pengalihan.
func (s *ShortenService) Redirect(ctx context.Context, code string) (string, error) {
	rec, err := s.repo.AddClick(ctx, code)
	if err != nil {
		return "", err
	}
	return rec.OriginalURL, nil
}

// Stats mengambil statistik satu short code tanpa menaikkan klik.
func (s *ShortenService) Stats(ctx context.Context, code string) (URLRecord, error) {
	return s.repo.FindByCode(ctx, code)
}

// ValidasiURL memeriksa URL asli dengan net/url.Parse: wajib berisi
// skema http/https dan host yang tidak kosong.
func ValidasiURL(raw string) error {
	if raw == "" {
		return ValidationError{Field: "url", Message: "wajib diisi"}
	}
	if len(raw) > maxPanjangURL {
		return ValidationError{Field: "url", Message: "melebihi 2048 karakter"}
	}
	u, err := url.Parse(raw)
	if err != nil {
		return ValidationError{Field: "url", Message: "format tidak valid"}
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return ValidationError{Field: "url", Message: "skema harus http atau https"}
	}
	if u.Host == "" {
		return ValidationError{Field: "url", Message: "host kosong"}
	}
	return nil
}

// alfabetBase62 berisi 62 karakter (a-z, A-Z, 0-9) yang aman
// dimasukkan ke URL tanpa perlu di-escape.
const alfabetBase62 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

// Base62Generator membuat short code acak sepanjang panjangKode dari
// alfabet base62. Sumber acaknya *rand.Rand (math/rand/v2) yang
// di-seed 16 byte crypto/rand, sehingga kode tidak bisa diprediksi
// dari kode-kode sebelumnya. Mutex diperlukan karena *rand.Rand
// tidak aman dipakai konkuren.
type Base62Generator struct {
	mu  sync.Mutex
	rng *rand.Rand
}

// NewBase62Generator membaca seed acak dari crypto/rand lalu membangun
// generator PCG.
func NewBase62Generator() (*Base62Generator, error) {
	seed := make([]byte, 16)
	if _, err := cryptorand.Read(seed); err != nil {
		return nil, fmt.Errorf("membaca seed crypto/rand: %w", err)
	}
	rng := rand.New(rand.NewPCG(
		binary.LittleEndian.Uint64(seed[:8]),
		binary.LittleEndian.Uint64(seed[8:]),
	))
	return &Base62Generator{rng: rng}, nil
}

// NewCode menghasilkan string acak dari alfabetBase62.
func (g *Base62Generator) NewCode() (string, error) {
	g.mu.Lock()
	defer g.mu.Unlock()
	buf := make([]byte, panjangKode)
	for i := range buf {
		buf[i] = alfabetBase62[g.rng.IntN(len(alfabetBase62))]
	}
	return string(buf), nil
}
