// 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 (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"

	"go.stargrave.org/tofuproxy/caches"
	"go.stargrave.org/tofuproxy/fifos"
	"go.stargrave.org/ucspi/v3"
)

var CCerts string

type ClientCertificateGetter struct {
	host string
	auth bool
}

func (g *ClientCertificateGetter) get(
	cri *tls.CertificateRequestInfo,
) (*tls.Certificate, error) {
	caches.TLSAuthCacheM.RLock()
	tlsCert := caches.TLSAuthCache[g.host]
	caches.TLSAuthCacheM.RUnlock()
	if tlsCert != nil {
		return tlsCert, nil
	}
	sigSchemes := make([]string, 0, len(cri.SignatureSchemes))
	for _, ss := range cri.SignatureSchemes {
		sigSchemes = append(sigSchemes, ss.String())
	}
	var b bytes.Buffer
	fmt.Fprintf(&b, `
wm title . "TLS client authentication: %s"

set lb [listbox .lb]
.lb insert end ""
grid .lb

proc login {} {
    global lb
    puts [$lb get active]
    exit
}

button .login -text "Use" -command login
grid .login

bind . <KeyPress> {switch -exact %%K {
    q {exit 0} ; # reject once
    n {puts "0:NONE" ; exit}
    l login
}}

label .lTLSVersion -text "TLS version: %s"
grid .lTLSVersion

set sigSchemeRow 0
foreach sigScheme {%s} {
    label .lSignatureScheme$sigSchemeRow -text "Signature scheme: $sigScheme"
    grid .lSignatureScheme$sigSchemeRow
    incr sigSchemeRow
}

`,
		g.host,
		ucspi.TLSVersion(cri.Version),
		strings.Join(sigSchemes, " "),
	)

	ents, err := os.ReadDir(CCerts)
	if err != nil {
		log.Fatalln(err)
	}
	certs := make([]*x509.Certificate, 0, len(ents))
	tlsCerts := make([]*tls.Certificate, 0, len(ents))
	b.WriteString(".lb insert end \"0: NONE\"\n")
	certs = append(certs, nil)
	tlsCerts = append(tlsCerts, nil)
	for i, ent := range ents {
		p := filepath.Join(CCerts, ent.Name())
		var cert *x509.Certificate
		_, cert, err = ucspi.CertificateFromFile(p)
		if err != nil {
			log.Fatalln(err)
		}
		var prv any
		prv, err = ucspi.PrivateKeyFromFile(p)
		if err != nil {
			log.Fatalln(err)
		}
		certs = append(certs, cert)
		tlsCerts = append(tlsCerts, &tls.Certificate{
			Certificate: [][]byte{cert.Raw},
			PrivateKey:  prv,
		})
		fmt.Fprintf(&b, ".lb insert end \"%d: %s\"\n", i+1, cert.Subject)
	}
	// os.WriteFile("/tmp/tls-auth-dialog.tcl", b.Bytes(), 0666)
	cmd := exec.Command(CmdWish)
	cmd.Stdin = &b
	out, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	lines := strings.Split(string(out), "\n")
	if len(lines) < 1 {
		return nil, errors.New("invalid output from authentication form")
	}
	t, _, _ := strings.Cut(lines[0], ":")
	i, err := strconv.Atoi(t)
	if err != nil {
		return &tls.Certificate{}, nil
	}
	if i == 0 {
		dummy := tls.Certificate{}
		caches.TLSAuthCacheM.Lock()
		caches.TLSAuthCache[g.host] = &dummy
		caches.TLSAuthCacheM.Unlock()
		return &dummy, nil
	}
	fifos.LogTLSAuth <- fmt.Sprintf("%s\t%s", g.host, certs[i].Subject)
	caches.TLSAuthCacheM.Lock()
	caches.TLSAuthCache[g.host] = tlsCerts[i]
	caches.TLSAuthCacheM.Unlock()
	g.auth = true
	return tlsCerts[i], nil
}
