// tofuproxy -- flexible HTTP/HTTPS proxy, TLS terminator, X.509 TOFU
//              manager, WARC/geminispace browser
// Copyright (C) 2021-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package tofuproxy

import (
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/dustin/go-humanize"
	"go.stargrave.org/tofuproxy/caches"
	"go.stargrave.org/tofuproxy/fifos"
	"go.stargrave.org/tofuproxy/rounds"
	ttls "go.stargrave.org/tofuproxy/tls"
)

var (
	transport = http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   time.Minute,
			KeepAlive: time.Minute,
		}).DialContext,
		MaxIdleConns:        http.DefaultTransport.(*http.Transport).MaxIdleConns,
		IdleConnTimeout:     http.DefaultTransport.(*http.Transport).IdleConnTimeout * 2,
		TLSHandshakeTimeout: time.Minute,
		DialTLSContext:      ttls.DialTLS,
		ForceAttemptHTTP2:   true,
	}
	proxyHeaders = map[string]struct{}{
		"Location":       {},
		"Content-Type":   {},
		"Content-Length": {},
	}
)

type Round func(
	host string,
	resp *http.Response,
	w http.ResponseWriter,
	req *http.Request,
) (bool, error)

func roundTrip(w http.ResponseWriter, req *http.Request) {
	defer req.Body.Close()
	fifos.LogReq <- fmt.Sprintf("%s %s", req.Method, req.URL)
	host := strings.TrimSuffix(req.URL.Host, ":443")
	for _, round := range []Round{
		rounds.RoundGemini,
		rounds.RoundWARC,
		rounds.RoundDenySpy,
		rounds.RoundHabrImage,
	} {
		if cont, _ := round(host, nil, w, req); !cont {
			return
		}
	}

	reqFlags := []string{}
	unauthorized := false

	caches.HTTPAuthCacheM.RLock()
	if creds, ok := caches.HTTPAuthCache[req.URL.Host]; ok {
		req.SetBasicAuth(creds[0], creds[1])
		unauthorized = true
	}
	caches.HTTPAuthCacheM.RUnlock()

Retry:
	resp, err := transport.RoundTrip(req)
	if err != nil {
		fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
		http.Error(w, err.Error(), http.StatusBadGateway)
		return
	}

	if resp.StatusCode == http.StatusUnauthorized {
		resp.Body.Close()
		caches.HTTPAuthCacheM.Lock()
		if unauthorized {
			delete(caches.HTTPAuthCache, req.URL.Host)
		} else {
			unauthorized = true
		}
		fifos.LogVarious <- fmt.Sprintf(
			"%s %s\tHTTP authorization required", req.Method, req.URL.Host,
		)
		var user, pass string
		user, pass, err = authDialog(host, resp.Header.Get("WWW-Authenticate"))
		if err != nil {
			caches.HTTPAuthCacheM.Unlock()
			fifos.LogErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error())
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		caches.HTTPAuthCache[req.URL.Host] = [2]string{user, pass}
		caches.HTTPAuthCacheM.Unlock()
		req.SetBasicAuth(user, pass)
		fifos.LogHTTPAuth <- fmt.Sprintf("%s %s\t%s", req.Method, req.URL, user)
		goto Retry
	}
	if unauthorized {
		reqFlags = append(reqFlags, "auth")
	}
	if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" {
		reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol)
	}

	for k, vs := range resp.Header {
		if _, ok := proxyHeaders[k]; ok {
			continue
		}
		for _, v := range vs {
			w.Header().Add(k, v)
		}
	}

	{
		var cont bool
		for _, round := range []Round{
			rounds.RoundDenyFonts,
			rounds.RoundTranscodeWebP,
			rounds.RoundTranscodeJXL,
			rounds.RoundTranscodeAVIF,
			rounds.RoundRedirectHTML,
		} {
			cont, err = round(host, resp, w, req)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadGateway)
				return
			}
			if !cont {
				return
			}
		}
	}

	for h := range proxyHeaders {
		if v := resp.Header.Get(h); v != "" {
			w.Header().Add(h, v)
		}
	}
	w.WriteHeader(resp.StatusCode)
	n, err := io.Copy(w, resp.Body)
	if err != nil {
		log.Printf("Error during %s: %+v\n", req.URL, err)
	}
	resp.Body.Close()
	msg := fmt.Sprintf(
		"%s %s\t%s\t%s\t%s\t%s",
		req.Method, req.URL,
		resp.Status,
		resp.Header.Get("Content-Type"),
		humanize.IBytes(uint64(n)),
		strings.Join(reqFlags, ","),
	)
	if resp.StatusCode == http.StatusOK {
		fifos.LogOK <- msg
	} else {
		fifos.LogNonOK <- msg
	}
}
