package main

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"time"
)

// waktuSekarang dibekukan agar contoh deterministik; produksi memakai time.Now().
var waktuSekarang = time.Date(2026, 1, 15, 8, 0, 0, 0, time.UTC)

type sesiPengguna struct {
	nama     string
	berakhir time.Time
}

// penyimpananSesi menyimpan sesi di memori. Mutex wajib karena map
// tidak aman dibaca dan ditulis banyak goroutine secara bersamaan.
type penyimpananSesi struct {
	mu   sync.Mutex
	sesi map[string]sesiPengguna
}

// buat membuat session ID acak 32 byte lalu menyimpan datanya.
func (p *penyimpananSesi) buat(nama string, umur time.Duration) (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	id := hex.EncodeToString(b)
	p.mu.Lock()
	p.sesi[id] = sesiPengguna{nama: nama, berakhir: waktuSekarang.Add(umur)}
	p.mu.Unlock()
	return id, nil
}

// ambil mengembalikan data sesi hanya bila ID ada dan belum kedaluwarsa.
func (p *penyimpananSesi) ambil(id string) (sesiPengguna, bool) {
	p.mu.Lock()
	defer p.mu.Unlock()
	data, ok := p.sesi[id]
	if !ok || !waktuSekarang.Before(data.berakhir) {
		return sesiPengguna{}, false
	}
	return data, true
}

func (p *penyimpananSesi) hapus(id string) {
	p.mu.Lock()
	delete(p.sesi, id)
	p.mu.Unlock()
}

type aplikasi struct {
	store penyimpananSesi
}

// kredensial contoh; produksi memverifikasi hash bcrypt, bukan pembandingan langsung.
var kredensial = map[string]string{
	"budi": "sandiRahasia123",
	"siti": "sandiSiti456",
}

func (a *aplikasi) login(w http.ResponseWriter, r *http.Request) {
	nama := r.PostFormValue("username")
	password := r.PostFormValue("password")
	if kredensial[nama] == "" || kredensial[nama] != password {
		http.Error(w, "kredensial salah", http.StatusUnauthorized)
		return
	}
	// Regenerasi: sesi lama dibuang supaya ID lama tidak dipakai ulang
	// (pertahanan terhadap session fixation).
	if c, err := r.Cookie("session_id"); err == nil {
		a.store.hapus(c.Value)
	}
	id, err := a.store.buat(nama, 30*time.Minute)
	if err != nil {
		http.Error(w, "gagal membuat sesi", http.StatusInternalServerError)
		return
	}
	http.SetCookie(w, &http.Cookie{
		Name:     "session_id",
		Value:    id,
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	http.Redirect(w, r, "/profil", http.StatusSeeOther)
}

func (a *aplikasi) profil(w http.ResponseWriter, r *http.Request) {
	c, err := r.Cookie("session_id")
	if err != nil {
		http.Error(w, "belum login", http.StatusUnauthorized)
		return
	}
	data, ok := a.store.ambil(c.Value)
	if !ok {
		http.Error(w, "sesi tidak valid", http.StatusUnauthorized)
		return
	}
	fmt.Fprintf(w, "pengguna: %s", data.nama)
}

func (a *aplikasi) logout(w http.ResponseWriter, r *http.Request) {
	if c, err := r.Cookie("session_id"); err == nil {
		a.store.hapus(c.Value)
	}
	// MaxAge negatif memerintahkan browser menghapus cookie.
	http.SetCookie(w, &http.Cookie{Name: "session_id", Value: "", Path: "/", MaxAge: -1})
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

// kirim menjalankan satu permintaan lalu mengembalikan status dan isi respons.
func kirim(client *http.Client, method, target, form string) (int, string) {
	var b io.Reader
	if form != "" {
		b = strings.NewReader(form)
	}
	req, err := http.NewRequest(method, target, b)
	if err != nil {
		log.Fatal(err)
	}
	if form != "" {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()
	isi, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(isi)
}

func cookieSesi(jar *cookiejar.Jar, serverURL string) string {
	u, err := url.Parse(serverURL)
	if err != nil {
		log.Fatal(err)
	}
	for _, c := range jar.Cookies(u) {
		if c.Name == "session_id" {
			return c.Value
		}
	}
	return ""
}

func main() {
	a := &aplikasi{store: penyimpananSesi{sesi: map[string]sesiPengguna{}}}
	mux := http.NewServeMux()
	mux.HandleFunc("POST /login", a.login)
	mux.HandleFunc("GET /profil", a.profil)
	mux.HandleFunc("POST /logout", a.logout)
	server := httptest.NewServer(mux)
	defer server.Close()

	jar, err := cookiejar.New(nil)
	if err != nil {
		log.Fatal(err)
	}
	// Jangan ikuti redirect agar kode 303 terlihat pada output.
	teruskan := func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse
	}
	// Klien dengan cookie jar seperti browser, dan klien polos tanpa cookie.
	clientBudi := &http.Client{Jar: jar, CheckRedirect: teruskan}
	clientPolos := &http.Client{CheckRedirect: teruskan}

	kode, _ := kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=passwordSalah")
	fmt.Printf("skenario  1: login dengan password salah            -> %d\n", kode)

	kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
	fmt.Printf("skenario  2: login dengan kredensial benar          -> %d\n", kode)

	idLama := cookieSesi(jar, server.URL)
	fmt.Printf("             cookie session_id terbit, panjang ID: %d karakter hex\n", len(idLama))

	kode, isi := kirim(clientBudi, "GET", server.URL+"/profil", "")
	fmt.Printf("skenario  3: akses /profil dengan cookie sesi       -> %d (%s)\n", kode, isi)

	kode, _ = kirim(clientPolos, "GET", server.URL+"/profil", "")
	fmt.Printf("skenario  4: akses /profil tanpa cookie             -> %d\n", kode)

	reqPalsu, err := http.NewRequest("GET", server.URL+"/profil", nil)
	if err != nil {
		log.Fatal(err)
	}
	reqPalsu.AddCookie(&http.Cookie{Name: "session_id", Value: strings.Repeat("61", 32)})
	respPalsu, err := clientPolos.Do(reqPalsu)
	if err != nil {
		log.Fatal(err)
	}
	respPalsu.Body.Close()
	fmt.Printf("skenario  5: akses /profil dengan ID dipalsukan     -> %d\n", respPalsu.StatusCode)

	kode, _ = kirim(clientBudi, "POST", server.URL+"/login", "username=budi&password=sandiRahasia123")
	fmt.Printf("skenario  6: login ulang, ID sesi diregenerasi      -> %d\n", kode)

	idBaru := cookieSesi(jar, server.URL)
	fmt.Printf("             ID baru berbeda dari ID lama: %v\n", idBaru != idLama)

	reqLama, err := http.NewRequest("GET", server.URL+"/profil", nil)
	if err != nil {
		log.Fatal(err)
	}
	reqLama.AddCookie(&http.Cookie{Name: "session_id", Value: idLama})
	respLama, err := clientPolos.Do(reqLama)
	if err != nil {
		log.Fatal(err)
	}
	respLama.Body.Close()
	fmt.Printf("skenario  7: akses /profil dengan ID lama           -> %d\n", respLama.StatusCode)

	kode, _ = kirim(clientBudi, "POST", server.URL+"/logout", "")
	fmt.Printf("skenario  8: logout                                 -> %d\n", kode)

	kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
	fmt.Printf("skenario  9: akses /profil setelah logout           -> %d\n", kode)

	kirim(clientBudi, "POST", server.URL+"/login", "username=siti&password=sandiSiti456")
	waktuSekarang = waktuSekarang.Add(31 * time.Minute)
	kode, _ = kirim(clientBudi, "GET", server.URL+"/profil", "")
	fmt.Printf("skenario 10: sesi Siti kedaluwarsa (waktu maju)     -> %d\n", kode)
}
