package main

import (
	"bufio"
	"bytes"
	"cmp"
	"context"
	"encoding/json"
	jsonv2 "encoding/json/v2"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"log/slog"
	"maps"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"testing/iotest"
	"time"
)

type Celsius float64

func (c Celsius) String() string { return strconv.FormatFloat(float64(c), 'f', 1, 64) + "°C" }
func must(err error) {
	if err != nil {
		panic(err)
	}
}
func heading(s string) { fmt.Println("==", s, "==") }

func textDemo() {
	heading("fmt")
	p := struct {
		Name  string
		Count int
	}{"book", 2}
	fmt.Printf("%v | %+v | %T\n", p, p, p)
	fmt.Printf("%q | %04d | %x\n", "a\nb", 7, "Go")
	format := "%d" // Runtime format mismatch; the static counterexample is in testdata.
	fmt.Println(fmt.Sprintf(format, "seven"))
	fmt.Println(Celsius(21.5))

	heading("strings")
	fmt.Println(strings.TrimRight("log.go", ".go"), strings.TrimSuffix("log.go", ".go"))
	key, value, found := strings.Cut("limit=25", "=")
	fmt.Println(key, value, found)
	fmt.Println(len(strings.Split("", ",")), len(strings.Fields("")))
	var b strings.Builder
	b.Grow(8)
	b.WriteString("go")
	b.WriteByte('-')
	b.WriteString("team")
	fmt.Println(b.String())

	heading("strconv")
	_, err := strconv.Atoi(" 25")
	fmt.Println("syntax", errors.Is(err, strconv.ErrSyntax))
	n, err := strconv.ParseInt("256", 10, 8)
	fmt.Println(n, errors.Is(err, strconv.ErrRange))
	fmt.Println(string(rune(66)), strconv.Itoa(66))
	fmt.Println(strconv.FormatFloat(1.25, 'f', 2, 64), strconv.Quote("a\nb"))

	heading("bytes")
	var buf bytes.Buffer
	buf.WriteString("draft")
	alias := buf.Bytes()
	saved := bytes.Clone(alias)
	buf.Reset()
	buf.WriteString("FINAL")
	fmt.Println(string(alias), string(saved))
}

func timeDemo() {
	heading("time")
	t := time.Date(2026, 10, 13, 9, 30, 0, 0, time.UTC)
	fmt.Println(t.Format("YYYY-MM-DD"), t.Format(time.RFC3339))
	east := time.FixedZone("UTC+8", 8*60*60)
	same := t.In(east)
	fmt.Println(t == same, t.Equal(same))
	a, err := time.Parse(time.DateTime, "2026-10-13 09:30:00")
	must(err)
	b, err := time.ParseInLocation(time.DateTime, "2026-10-13 09:30:00", east)
	must(err)
	fmt.Println(a.Sub(b))
	var d time.Duration = 3
	fmt.Println(d, 3*time.Second)
	now := time.Now()
	fmt.Println(now == now.Round(0), now.Equal(now.Round(0)))
	timer := time.NewTimer(time.Hour)
	fmt.Println("timer", cap(timer.C), timer.Stop())
}

type eofReader struct{ offset int }

func (r *eofReader) Read(p []byte) (int, error) {
	n := copy(p, "ok"[r.offset:])
	r.offset += n
	if r.offset == 2 {
		return n, io.EOF
	}
	return n, nil
}

var ErrTooLarge = errors.New("input exceeds limit")

func ReadBounded(r io.Reader, max int64) ([]byte, error) {
	if max < 0 || max == int64(^uint64(0)>>1) {
		return nil, errors.New("invalid limit")
	}
	data, err := io.ReadAll(io.LimitReader(r, max+1))
	if err != nil {
		return nil, err
	}
	if int64(len(data)) > max {
		return nil, ErrTooLarge
	}
	return data, nil
}
func scanLengths(input string, max int) ([]int, error) {
	sc := bufio.NewScanner(strings.NewReader(input))
	if max > 0 {
		sc.Buffer(make([]byte, 0, 4096), max)
	}
	var lengths []int
	for sc.Scan() {
		lengths = append(lengths, len(sc.Text()))
	}
	return lengths, sc.Err()
}
func ioDemo() {
	heading("io")
	r := iotest.OneByteReader(strings.NewReader("ready"))
	p := make([]byte, 5)
	n, err := r.Read(p)
	fmt.Println(n, err)
	n, err = io.ReadFull(r, p[1:])
	fmt.Println(n, err, string(p))
	n, err = (&eofReader{}).Read(p)
	fmt.Println(string(p[:n]), err == io.EOF)
	_, err = ReadBounded(strings.NewReader("12345"), 4)
	fmt.Println("too large", errors.Is(err, ErrTooLarge))
	var copy1, copy2 bytes.Buffer
	n64, err := io.Copy(io.MultiWriter(&copy1, &copy2), strings.NewReader("go"))
	fmt.Println(n64, err, copy1.String(), copy2.String())

	heading("bufio")
	var out bytes.Buffer
	w := bufio.NewWriter(&out)
	_, err = w.WriteString("ready")
	must(err)
	fmt.Println("before", out.Len())
	must(w.Flush())
	fmt.Println("after", out.Len())
	input := "first\n" + strings.Repeat("x", 70_000) + "\nlast\n"
	lengths, err := scanLengths(input, 0)
	fmt.Println(lengths, err)
	lengths, err = scanLengths(input, 1<<20)
	fmt.Println(lengths, err)
}

func osDemo() {
	heading("os")
	dir, err := os.MkdirTemp("", "stdliblab-")
	must(err)
	defer os.RemoveAll(dir)
	path := filepath.Join(dir, "note.txt")
	must(os.WriteFile(path, []byte("old"), 0600))
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0600)
	must(err)
	_, err = f.WriteString("+new")
	closeErr := f.Close()
	must(err)
	must(closeErr)
	data, err := os.ReadFile(path)
	must(err)
	fmt.Println(string(data))
	f, err = os.Create(path)
	must(err)
	must(f.Close())
	data, err = os.ReadFile(path)
	must(err)
	fmt.Println("after Create", len(data))
	f, err = os.Open(path)
	must(err)
	_, err = f.WriteString("x")
	must(f.Close())
	fmt.Println("read-only write failed", err != nil)
	_, err = os.ReadFile(filepath.Join(dir, "missing"))
	fmt.Println("not found", errors.Is(err, fs.ErrNotExist))
	const key = "STDLIBLAB_LOCAL_EMPTY"
	previous, existed := os.LookupEnv(key)
	defer func() {
		if existed {
			must(os.Setenv(key, previous))
		} else {
			must(os.Unsetenv(key))
		}
	}()
	must(os.Setenv(key, ""))
	v, ok := os.LookupEnv(key)
	fmt.Printf("empty %q %v\n", v, ok)
	must(os.Unsetenv(key))
	v, ok = os.LookupEnv(key)
	fmt.Printf("unset %q %v\n", v, ok)
}

type User struct {
	ID      int64     `json:"id"`
	Name    string    `json:"name"`
	Tags    []string  `json:"tags"`
	Created time.Time `json:"created,omitempty"`
	Updated time.Time `json:"updated,omitzero"`
	email   string
}

// DecodeOne expects exactly one JSON value, with no unknown struct fields.
func DecodeOne(r io.Reader, dst any) error {
	dec := json.NewDecoder(r)
	dec.DisallowUnknownFields()
	if err := dec.Decode(dst); err != nil {
		return err
	}
	var extra any
	if err := dec.Decode(&extra); err != io.EOF {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

func jsonDemo() {
	heading("json")
	u := User{ID: 1, Name: "<reader>", email: "private@example.invalid"}
	b, err := json.Marshal(u)
	must(err)
	fmt.Println(string(b))
	raw := `{"id":9007199254740993,"name":"Ada"}`
	var dynamic map[string]any
	must(json.Unmarshal([]byte(raw), &dynamic))
	fmt.Printf("%T %.0f\n", dynamic["id"], dynamic["id"])
	var typed User
	must(json.Unmarshal([]byte(raw), &typed))
	fmt.Println(typed.ID)
	dec := json.NewDecoder(strings.NewReader(raw))
	dec.UseNumber()
	must(dec.Decode(&dynamic))
	fmt.Printf("%T %v\n", dynamic["id"], dynamic["id"])
	dec = json.NewDecoder(strings.NewReader(`{"name":"Ada","extra":1}`))
	dec.DisallowUnknownFields()
	fmt.Println("unknown rejected", dec.Decode(&typed) != nil)
	dec = json.NewDecoder(strings.NewReader(`{"name":"Ada"} {"name":"Lin"}`))
	must(dec.Decode(&typed))
	var extra any
	fmt.Println("second JSON", dec.Decode(&extra) == nil)
	b, err = jsonv2.Marshal(u)
	must(err)
	fmt.Println(string(b))
	var v1, v2 User
	must(json.Unmarshal([]byte(`{"ID":9}`), &v1))
	must(jsonv2.Unmarshal([]byte(`{"ID":9}`), &v2))
	fmt.Println("case", v1.ID, v2.ID)
	var duplicate map[string]int
	fmt.Println("v2 duplicate rejected", jsonv2.Unmarshal([]byte(`{"x":1,"x":2}`), &duplicate) != nil)
}

func httpDemo() {
	heading("net/http")
	mux := http.NewServeMux()
	mux.HandleFunc("GET /greet/{name}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		fmt.Fprint(w, "hello "+r.PathValue("name"))
	})
	server := httptest.NewServer(mux)
	defer server.Close()
	client := &http.Client{Timeout: time.Second}
	defer client.CloseIdleConnections()
	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL+"/greet/Ada", nil)
	must(err)
	resp, err := client.Do(req)
	must(err)
	body, readErr := ReadBounded(resp.Body, 1024)
	closeErr := resp.Body.Close()
	must(readErr)
	must(closeErr)
	fmt.Println(resp.StatusCode, string(body))
	resp, err = client.Get(server.URL + "/missing")
	must(err)
	_, readErr = io.Copy(io.Discard, resp.Body)
	closeErr = resp.Body.Close()
	must(readErr)
	must(closeErr)
	fmt.Println("missing", resp.StatusCode, err == nil)
	fmt.Println("default timeout", http.DefaultClient.Timeout)
}

func contextDemo() {
	heading("context")
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	<-ctx.Done()
	fmt.Println(errors.Is(ctx.Err(), context.Canceled))
	past, release := context.WithDeadline(context.Background(), time.Unix(0, 0))
	defer release()
	<-past.Done()
	fmt.Println(errors.Is(past.Err(), context.DeadlineExceeded))
}

func syncDemo() {
	heading("sync")
	var mu sync.Mutex
	var wg sync.WaitGroup
	total := 0
	for range 3 {
		wg.Go(func() { mu.Lock(); defer mu.Unlock(); total++ })
	}
	wg.Wait()
	fmt.Println("total", total)
	var once sync.Once
	count := 0
	for range 3 {
		once.Do(func() { count++ })
	}
	fmt.Println("once", count)
}

func errorsDemo() {
	heading("errors")
	wrapped := fmt.Errorf("load settings: %w", fs.ErrNotExist)
	fmt.Println(wrapped == fs.ErrNotExist, errors.Is(wrapped, fs.ErrNotExist))
	pathErr := &os.PathError{Op: "open", Path: "settings.json", Err: fs.ErrPermission}
	joined := errors.Join(wrapped, pathErr)
	var target *os.PathError
	fmt.Println(errors.As(joined, &target), target.Op, target.Path)
	typed, ok := errors.AsType[*os.PathError](joined)
	fmt.Println(ok, typed.Op, errors.Is(joined, fs.ErrPermission))
}

type Secret string

func (Secret) LogValue() slog.Value { return slog.StringValue("[REDACTED]") }
func newLogger(w io.Writer) *slog.Logger {
	return slog.New(slog.NewJSONHandler(w, &slog.HandlerOptions{
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if len(groups) == 0 && a.Key == slog.TimeKey {
				return slog.Attr{}
			}
			return a
		},
	}))
}
func slogDemo() {
	heading("slog")
	logger := newLogger(os.Stdout).With("service", "demo")
	logger.Info("accepted", slog.Int("count", 2), slog.Any("token", Secret("sample-secret")))
	logger.Debug("hidden by default")
	args := []any{"orphan"}
	logger.Info("bad fields", args...)
}

func collectionsDemo() {
	heading("sort")
	xs := []int{1, 4, 8}
	fmt.Println(sort.SearchInts(xs, 6), sort.SearchInts(xs, 10))
	fmt.Println(slices.BinarySearch(xs, 6))
	heading("slices")
	all := []int{1, 2, 3, 4, 5}
	after := slices.Delete(all, 1, 3)
	fmt.Println(after, all)
	type person struct {
		Name string
		Age  int
	}
	people := []person{{"Lin", 30}, {"Ada", 30}, {"Bo", 20}}
	slices.SortFunc(people, func(a, b person) int { return cmp.Or(cmp.Compare(a.Age, b.Age), cmp.Compare(a.Name, b.Name)) })
	fmt.Println(people)
	heading("maps")
	m := map[string][]int{"b": {2}, "a": {1}}
	fmt.Println(slices.Sorted(maps.Keys(m)))
	cloned := maps.Clone(m)
	cloned["a"][0] = 7
	fmt.Println(m["a"], cloned["a"])
}

func main() {
	textDemo()
	timeDemo()
	ioDemo()
	osDemo()
	jsonDemo()
	httpDemo()
	contextDemo()
	syncDemo()
	errorsDemo()
	slogDemo()
	collectionsDemo()
}
