package github

import (
	"crypto/tls"
	"crypto/x509"
	"net/http"
	"os"
	"sync"
	"time"

	gh "github.com/google/go-github/v72/github"
)

// mintClientTimeout bounds a single App-JWT / installation-token HTTP call.
// These are point calls to GitHub (or the MITM proxy in front of it); a stuck
// dial must not pin the token-refresh path indefinitely. Matches the 30s used
// by the App client transport in NewClientFromApp.
const mintClientTimeout = 30 * time.Second

// The chicken-and-egg this file breaks
// ------------------------------------
// When F5 forced egress is on, every outbound :443 (including this package's
// GitHub App token-mint calls to POST /app/installations/{id}/access_tokens) is
// iptables-REDIRECTed to the in-process MITM proxy, which presents a leaf cert
// forged by the proxy's own CA at /data/proxy-ca.pem. Go's crypto/x509 must
// trust that CA or every mint fails "x509: certificate signed by unknown
// authority" → "all N repos failed" → the agent loop is dead.
//
// The proxy CA is GENERATED BY the Go proxy at runtime (proxy.NewGitHubProxy →
// loadOrGenerateCA writes /data/proxy-ca.pem). On a fresh-PVC boot that file
// does NOT exist yet when the process starts, so:
//
//   - The entrypoint's SSL_CERT_FILE combined-bundle build (#2686) is gated on
//     `[ -f /data/proxy-ca.pem ]`, which is FALSE on first boot → SSL_CERT_FILE
//     is never exported → the Go process falls back to the system trust store.
//   - The system store's own CA-install (gosu update-ca-certificates) also runs
//     AFTER launch and can fail, leaving a multi-minute window after every pod
//     roll where token minting fails until the system CA eventually settles.
//
// Fix (Option B, race-free): the very process that GENERATES the proxy CA also
// establishes the trust for it. Instead of depending on an env var set by the
// entrypoint before launch, the token-mint HTTP client below carries its own
// RootCAs pool = system roots + the proxy CA, and it LAZILY (re)loads the proxy
// CA from disk whenever the file appears or changes. So the first mint after
// the proxy has written /data/proxy-ca.pem trusts it immediately — no
// dependency on entrypoint ordering, no cached-empty-pool window.
//
// The pool always includes the SYSTEM roots too, so advisory mode (no forced
// egress: the mint reaches api.github.com directly with a real public cert)
// keeps working. This is the same "trust BOTH public roots AND proxy CA"
// safety the entrypoint's combined bundle provides.

// proxyCAPath is the PVC location of the MITM CA cert the in-process proxy
// generates (must match proxy.CACertPath). A var so tests can redirect it.
var proxyCAPath = "/data/proxy-ca.pem"

// proxyCAReloadInterval bounds how often the disk is stat/re-read. Minting is
// infrequent (a token lasts ~1h and is refreshed within a 20m buffer), so a
// short interval costs nothing and lets a freshly-written CA be picked up
// promptly on first boot without a per-call read.
const proxyCAReloadInterval = 5 * time.Second

// proxyTrustPool builds and caches a RootCAs pool of (system roots + proxy CA),
// reloading the proxy CA from disk when the file changes so a CA written after
// process start (the fresh-PVC boot case) is trusted on the next mint.
type proxyTrustPool struct {
	mu         sync.Mutex
	pool       *x509.CertPool
	caModTime  time.Time
	caSize     int64
	lastReload time.Time
}

// certPool returns a RootCAs pool trusting the system roots plus the proxy CA
// if present. It re-reads /data/proxy-ca.pem at most once per
// proxyCAReloadInterval, and only rebuilds the pool when the file's
// size/mtime changed, so the steady-state cost is a single stat.
//
// A nil return means "use Go's default (system) trust" — callers pass it
// straight into tls.Config.RootCAs, where nil already means system roots, so
// the system-roots path is never broken even if reading the CA fails.
func (t *proxyTrustPool) certPool() *x509.CertPool {
	t.mu.Lock()
	defer t.mu.Unlock()

	now := time.Now()
	if t.pool != nil && now.Sub(t.lastReload) < proxyCAReloadInterval {
		return t.pool
	}
	t.lastReload = now

	info, err := os.Stat(proxyCAPath)
	if err != nil {
		// No proxy CA on disk yet (fresh boot, before the proxy generated it,
		// or advisory mode where it may never exist). Fall back to system roots
		// only. Returning the previously-built pool (if any) would also be
		// correct, but system-only is the honest state until the CA appears.
		if t.pool != nil {
			// Keep a previously-built (system + CA) pool if the file merely
			// became briefly unreadable; do not downgrade an established trust.
			return t.pool
		}
		return systemOnlyPool()
	}

	// Rebuild only when the file actually changed since we last loaded it.
	if t.pool != nil && info.ModTime().Equal(t.caModTime) && info.Size() == t.caSize {
		return t.pool
	}

	caPEM, err := os.ReadFile(proxyCAPath)
	if err != nil {
		if t.pool != nil {
			return t.pool
		}
		return systemOnlyPool()
	}

	pool := systemOnlyPool()
	if pool == nil {
		pool = x509.NewCertPool()
	}
	if !pool.AppendCertsFromPEM(caPEM) {
		// Unparseable CA file: keep whatever trust we already had rather than
		// serving an empty/partial pool.
		if t.pool != nil {
			return t.pool
		}
		return systemOnlyPool()
	}

	t.pool = pool
	t.caModTime = info.ModTime()
	t.caSize = info.Size()
	return t.pool
}

// systemOnlyPool returns a clone of the system cert pool, or nil (which
// tls.Config treats as "system roots") when the system pool is unavailable.
func systemOnlyPool() *x509.CertPool {
	if pool, err := x509.SystemCertPool(); err == nil {
		return pool
	}
	return nil
}

// sharedProxyTrust is the process-wide pool used by every JWT/token-mint client
// so the reload bookkeeping (and the system-pool load) is shared, not repeated
// per call.
var sharedProxyTrust = &proxyTrustPool{}

// proxyTrustingHTTPClient returns an *http.Client whose TLS RootCAs trust the
// system roots plus the in-process proxy CA (lazily reloaded from disk). This
// is the client the App-JWT / installation-token mint calls use so a fresh-PVC
// boot trusts the proxy CA from the FIRST mint attempt, independent of
// SSL_CERT_FILE or the entrypoint's launch ordering.
//
// The transport clones http.DefaultTransport (preserving proxy/keep-alive/
// timeout defaults) and installs the RootCAs pool. crypto/tls has no
// per-handshake roots hook, so the pool is captured at client construction —
// but a fresh client is built for each short-lived mint call, so the pool is
// re-read (subject to certPool's own reload interval) each time, and a CA that
// appears after process start is trusted on the next mint.
func proxyTrustingHTTPClient(timeout time.Duration) *http.Client {
	var transport *http.Transport
	if base, ok := http.DefaultTransport.(*http.Transport); ok {
		transport = base.Clone()
	} else {
		transport = &http.Transport{}
	}
	if transport.TLSClientConfig == nil {
		transport.TLSClientConfig = &tls.Config{}
	} else {
		transport.TLSClientConfig = transport.TLSClientConfig.Clone()
	}
	// certPool returns (system roots + proxy CA), or a system-only pool while
	// the CA does not yet exist. nil would also mean "system roots", so the
	// direct-HTTPS (advisory mode) path is never broken.
	transport.TLSClientConfig.RootCAs = sharedProxyTrust.certPool()
	return &http.Client{Transport: transport, Timeout: timeout}
}

// newTokenClient builds a go-github client authenticated with an installation
// token whose HTTP transport trusts the proxy CA. Used by NewClient so ordinary
// API traffic (which is also proxied under forced egress) verifies correctly.
func newTokenClient(token, apiURL string) *gh.Client {
	client := gh.NewClient(proxyTrustingHTTPClient(mintClientTimeout)).WithAuthToken(token)
	setBaseURL(client, apiURL)
	return client
}
