// Copyright 2026 printing-press-golden and contributors. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package client

import (
	"encoding/json"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"golden-api-cookie-auth-pp-cli/internal/cliutil"
)

// cookieJar wraps an http.CookieJar so writes (server Set-Cookie response
// headers) persist to disk. The CLI loads existing cookies on every client
// construction and writes back after every request that carries Set-Cookie
// response headers, so a logged-in session survives across invocations.
type cookieJar struct {
	inner http.CookieJar
	path  string
	mu    sync.Mutex
}

type persistedCookie struct {
	Name    string    `json:"name"`
	Value   string    `json:"value"`
	Domain  string    `json:"domain"`
	Path    string    `json:"path"`
	Expires time.Time `json:"expires,omitempty"`
	Secure  bool      `json:"secure"`
	HTTP    bool      `json:"http_only"`
}

// LoadCookieJar returns a persistent cookie jar pre-populated from the canonical
// cookie file on disk. Falls back to an empty in-memory jar when no file exists.
func LoadCookieJar() http.CookieJar {
	inner, _ := cookiejar.New(nil)
	path := cookieJarPath()
	jar := &cookieJar{inner: inner, path: path}
	jar.loadFromDisk()
	return jar
}

func cookieJarPath() string {
	dir, err := cliutil.DataDir()
	if err != nil {
		return ""
	}
	return filepath.Join(dir, "cookies.json")
}

// looksLikeCookieJar reports whether s is a cookie-jar string ("name=value;
// name=value") rather than a bare token. The session env var and the browser
// AccessToken both store the full Cookie header, so the seed is gated on a real
// "name=value" pair. A bare "=" is too loose: a base64-padded JWT ("eyJ...Q==")
// contains "=" yet is a single bearer token, and strings.Cut would split it into
// a bogus {name:"eyJ...Q", value:"="} cookie. So require the first segment to be
// a valid name=value pair whose name is a legal cookie-name token; that screens
// out JWT/bearer values while still passing a single legit "name=value" cookie.
func looksLikeCookieJar(s string) bool {
	first, _, _ := strings.Cut(s, ";")
	name, value, ok := strings.Cut(strings.TrimSpace(first), "=")
	if !ok || value == "" {
		return false
	}
	return isCookieName(strings.TrimSpace(name))
}

// isCookieName reports whether s is a non-empty RFC 6265 cookie-name token
// (RFC 2616 token: no controls, spaces, or separators). It additionally rejects
// "." and "/": these are valid token bytes but appear in JWT/bearer values
// (header.payload.signature, base64url "/"), which a padded "==" suffix would
// otherwise sneak past as a name=value pair. Server-set cookie names do not use
// them in practice, so screening them out separates a real session jar from a
// bearer token without rejecting legitimate single cookies.
func isCookieName(s string) bool {
	if s == "" {
		return false
	}
	for i := 0; i < len(s); i++ {
		b := s[i]
		if b <= 0x20 || b >= 0x7f {
			return false
		}
		switch b {
		case '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"',
			'/', '[', ']', '?', '=', '{', '}', '.':
			return false
		}
	}
	return true
}

// parseCookieJar splits a Cookie-header-style string ("name=value; name=value")
// into cookies. Pairs without "=" or with an empty name are skipped; values are
// sanitized to the bytes net/http's jar accepts.
func parseCookieJar(s string) []*http.Cookie {
	parts := strings.Split(s, ";")
	cookies := make([]*http.Cookie, 0, len(parts))
	for _, part := range parts {
		name, value, ok := strings.Cut(strings.TrimSpace(part), "=")
		name = strings.TrimSpace(name)
		if !ok || name == "" {
			continue
		}
		cookies = append(cookies, &http.Cookie{
			Name:  name,
			Value: sanitizeCookieValue(strings.TrimSpace(value)),
		})
	}
	return cookies
}

// SeedCookieJar seeds jar with the cookies in a Cookie-header-style credential
// string scoped to baseURL's host, so a session captured via the env var or
// stored in credentials (not the on-disk cookies.json) still rides every
// request. Seeding the jar (rather than setting a static Cookie header) lets
// net/http absorb Set-Cookie rotation — Cloudflare __cf_bm, AWS ALB AWSALB —
// across a multi-request session that a static header would let go stale.
// A no-op when the credential is empty, is not a cookie-jar string, or baseURL
// does not parse.
func SeedCookieJar(jar http.CookieJar, baseURL, cookieStr string) {
	if jar == nil || !looksLikeCookieJar(cookieStr) {
		return
	}
	u, err := url.Parse(baseURL)
	if err != nil || u.Host == "" {
		return
	}
	cookies := parseCookieJar(cookieStr)
	if len(cookies) == 0 {
		return
	}
	// Seed the in-memory jar only — never persist. The wrapper's SetCookies
	// writes through to cookies.json (persistLocked -> mergeAndWriteCookieRows),
	// which would clobber fresher rotation-refreshed values (Cloudflare __cf_bm,
	// AWS ALB AWSALB) already on disk with the stale env/credential ones. Seeding
	// the inner jar (as loadFromDisk does) keeps the credential live for the
	// session without touching the persisted set.
	if cj, ok := jar.(*cookieJar); ok {
		cj.inner.SetCookies(u, cookies)
		return
	}
	jar.SetCookies(u, cookies)
}

// sanitizeCookieValue strips bytes that net/http's cookie jar rejects per
// RFC 6265 (cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E;
// excludes whitespace, double-quote, comma, semicolon, backslash, and
// control characters). The jar's SetCookies silently drops invalid bytes
// and writes a warning to the standard logger on every invocation, which
// leaks to stderr on every CLI run. Chrome-captured session cookies for
// some providers (e.g. g_state, attentive_*) carry JSON-shaped values with
// internal double-quotes; sanitizing once at load time keeps the payload
// usable (the server stripped the quotes too) and silences the warning.
// Also removes a single layer of surrounding double-quotes that some
// Chrome extractions add to non-JSON values.
func sanitizeCookieValue(value string) string {
	if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
		value = value[1 : len(value)-1]
	}
	clean := make([]byte, 0, len(value))
	for i := 0; i < len(value); i++ {
		b := value[i]
		if b == 0x21 || (b >= 0x23 && b <= 0x2B) || (b >= 0x2D && b <= 0x3A) ||
			(b >= 0x3C && b <= 0x5B) || (b >= 0x5D && b <= 0x7E) {
			clean = append(clean, b)
		}
	}
	return string(clean)
}

// WriteCookieJarFromMap persists a name→value cookie set under the given
// domain. Called from auth.go after Chrome extraction so subsequent requests
// carry the user's session. Merges with any existing rows so server-pushed
// cookies stored by persistLocked survive an auth login/refresh.
func WriteCookieJarFromMap(domain string, cookies map[string]string) error {
	rows := make([]persistedCookie, 0, len(cookies))
	for name, value := range cookies {
		rows = append(rows, persistedCookie{
			Name:   name,
			Value:  sanitizeCookieValue(value),
			Domain: domain,
			Path:   "/",
			Secure: true,
		})
	}
	return mergeAndWriteCookieRows(cookieJarPath(), rows)
}

// mergeAndWriteCookieRows reads the on-disk cookie file, merges the new rows
// by the (domain, path, name) identity triple (RFC 6265 §5.3), and writes
// the union back atomically. Shared by the login flow (filtered required
// cookies) and the http.CookieJar interface (server Set-Cookie response
// headers) so neither write path silently clobbers the other.
func mergeAndWriteCookieRows(path string, rows []persistedCookie) error {
	existing, _ := os.ReadFile(path)
	var all []persistedCookie
	_ = json.Unmarshal(existing, &all)
	idx := make(map[string]int, len(all))
	for i, r := range all {
		idx[r.Domain+"|"+r.Path+"|"+r.Name] = i
	}
	for _, r := range rows {
		filtered := all[:0]
		for _, existing := range all {
			if shouldReplaceShadowingCookie(existing, r) {
				delete(idx, existing.Domain+"|"+existing.Path+"|"+existing.Name)
				continue
			}
			filtered = append(filtered, existing)
		}
		all = filtered
		idx = make(map[string]int, len(all))
		for i, existing := range all {
			idx[existing.Domain+"|"+existing.Path+"|"+existing.Name] = i
		}
		key := r.Domain + "|" + r.Path + "|" + r.Name
		if i, ok := idx[key]; ok {
			all[i] = r
			continue
		}
		all = append(all, r)
		idx[key] = len(all) - 1
	}
	data, err := json.MarshalIndent(all, "", "  ")
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o600)
}

func shouldReplaceShadowingCookie(existing, incoming persistedCookie) bool {
	if existing.Name != incoming.Name || existing.Path != incoming.Path {
		return false
	}
	return normalizedWWWCookieDomain(existing.Domain) == normalizedWWWCookieDomain(incoming.Domain)
}

func normalizedWWWCookieDomain(domain string) string {
	domain = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(domain)), ".")
	return strings.TrimPrefix(domain, "www.")
}

func (j *cookieJar) loadFromDisk() {
	data, err := os.ReadFile(j.path)
	if err != nil {
		return
	}
	var rows []persistedCookie
	if err := json.Unmarshal(data, &rows); err != nil {
		return
	}
	// Group cookies by host so a single SetCookies call carries the full
	// per-host set; the jar keys cookies by host so this matches its shape.
	byHost := map[string][]*http.Cookie{}
	for _, r := range rows {
		host := strings.TrimPrefix(r.Domain, ".")
		if host == "" {
			continue
		}
		byHost[host] = append(byHost[host], &http.Cookie{
			Name:    r.Name,
			Value:   sanitizeCookieValue(r.Value),
			Domain:  r.Domain,
			Path:    r.Path,
			Expires: r.Expires,
			Secure:  r.Secure,
		})
	}
	for host, cs := range byHost {
		u := &url.URL{Scheme: "https", Host: host}
		j.inner.SetCookies(u, cs)
	}
}

// Cookies / SetCookies — http.CookieJar interface; delegates to inner jar and
// persists writes.

func (j *cookieJar) Cookies(u *url.URL) []*http.Cookie {
	return j.inner.Cookies(u)
}

func (j *cookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
	j.inner.SetCookies(u, cookies)
	j.mu.Lock()
	defer j.mu.Unlock()
	j.persistLocked(u, cookies)
}

func (j *cookieJar) persistLocked(u *url.URL, cookies []*http.Cookie) {
	rows := make([]persistedCookie, 0, len(cookies))
	for _, c := range cookies {
		domain := c.Domain
		if domain == "" {
			domain = "." + u.Host
		}
		path := c.Path
		if path == "" {
			path = "/"
		}
		rows = append(rows, persistedCookie{
			Name:    c.Name,
			Value:   c.Value,
			Domain:  domain,
			Path:    path,
			Expires: c.Expires,
			Secure:  c.Secure,
			HTTP:    c.HttpOnly,
		})
	}
	_ = mergeAndWriteCookieRows(j.path, rows)
}
