// 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 rounds

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"html"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"

	"go.stargrave.org/tofuproxy/fifos"
	ttls "go.stargrave.org/tofuproxy/tls"
)

const (
	ContentTypeGemini = "text/gemini"
	SchemeGemini      = "gemini://"
	GeminiEntrypoint  = "https://gemini"
	GeminiPort        = ":1965"
)

var GemCodeName = map[int]string{
	10: "INPUT",
	11: "SENSITIVE INPUT",
	20: "SUCCESS",
	30: "REDIRECT - TEMPORARY",
	31: "REDIRECT - PERMANENT",
	40: "TEMPORARY FAILURE",
	41: "SERVER UNAVAILABLE",
	42: "CGI ERROR",
	43: "PROXY ERROR",
	44: "SLOW DOWN",
	50: "PERMANENT FAILURE",
	51: "NOT FOUND",
	52: "GONE",
	53: "PROXY REQUEST REFUSED",
	59: "BAD REQUEST",
	60: "CLIENT CERTIFICATE REQUIRED",
	61: "CERTIFICATE NOT AUTHORISED",
	62: "CERTIFICATE NOT VALID",
}

func absolutizeURL(host, u string, paths ...string) string {
	host = strings.TrimSuffix(host, GeminiPort)
	if strings.Contains(u, "://") {
		return u
	}
	if strings.HasPrefix(u, "/") {
		return GeminiEntrypoint + "/" + host + u
	}
	paths = append([]string{GeminiEntrypoint, host}, paths...)
	paths = append(paths, u)
	return strings.Join(paths, "/")
}

func geminifyURL(host, u string, paths ...string) string {
	u = absolutizeURL(host, u, paths...)
	if !strings.HasPrefix(u, SchemeGemini) {
		return u
	}
	return GeminiEntrypoint + "/" + strings.TrimPrefix(u, SchemeGemini)
}

func RoundGemini(
	host string,
	resp *http.Response,
	w http.ResponseWriter,
	req *http.Request,
) (bool, error) {
	if host != "gemini" {
		return true, nil
	}
	paths := strings.Split(strings.TrimPrefix(req.URL.Path, "/"), "/")
	host, paths = paths[0], paths[1:]
	if host == "gemini:" {
		http.Redirect(w, req, strings.Join(
			append([]string{GeminiEntrypoint}, paths[1:]...), "/",
		), http.StatusTemporaryRedirect)
		return false, nil
	}
	hostWithPort := host
	if !strings.Contains(hostWithPort, ":") {
		hostWithPort += GeminiPort
	}
	conn, err := ttls.DialTLS(context.TODO(), "tcp", hostWithPort)
	if err != nil {
		log.Printf("%s: can not dial: %+v\n", req.URL, err)
		return false, err
	}
	query := fmt.Sprintf("%s%s/%s", SchemeGemini, host, strings.Join(paths, "/"))
	if req.URL.RawQuery != "" {
		query += "?" + req.URL.RawQuery
	}
	if _, err = conn.Write([]byte(query + "\r\n")); err != nil {
		log.Printf("%s: can not send request: %+v\n", req.URL, err)
		return false, err
	}
	if len(paths) > 0 && paths[len(paths)-1] == "" {
		paths = paths[:len(paths)-1]
	}
	br := bufio.NewReader(conn)
	rawResp, err := br.ReadString('\n')
	if err != nil {
		log.Printf("%s: can not read response: %+v\n", req.URL, err)
		return false, err
	}
	cols := strings.SplitN(strings.TrimRight(rawResp, "\r\n"), " ", 2)
	if len(cols) < 2 {
		err = fmt.Errorf("invalid response format: %s", rawResp)
		log.Printf("%s: %s\n", req.URL, err)
		return false, err
	}
	code, err := strconv.Atoi(cols[0])
	if err != nil {
		log.Printf("%s: can not parse response code: %+v\n", req.URL, err)
		return false, err
	}
	codeName := GemCodeName[code]
	if codeName == "" {
		codeName = "UNKNOWN"
	}
	if 10 <= code && code <= 19 {
		w.Header().Add("Content-Type", "text/plain")
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintf(w, "%s\n%d (%s): INPUT is not supported\n", cols[1], code, codeName)
		return false, nil
	}
	if 30 <= code && code <= 39 {
		w.Header().Add("Content-Type", "text/html")
		w.WriteHeader(http.StatusOK)
		u := geminifyURL(host, cols[1], paths...)
		fmt.Fprintf(w, `<!DOCTYPE html>
<html><head><title>%d (%s) redirection</title></head>
<body><a href="%s">%s</a></body></html>`, code, codeName, u, u)
		fifos.LogRedir <- fmt.Sprintf(
			"%s %s\t%d\t%s", req.Method, req.URL, code, cols[1],
		)
		return false, nil
	}
	msg := fmt.Sprintf(
		"%s %s\t%d (%s)\t%s",
		req.Method, req.URL,
		code, codeName,
		cols[1],
	)
	if 40 <= code && code <= 49 {
		w.Header().Add("Content-Type", "text/plain")
		w.WriteHeader(http.StatusBadGateway)
		fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
		fifos.LogNonOK <- msg
		return false, nil
	}
	if 50 <= code && code <= 59 {
		w.Header().Add("Content-Type", "text/plain")
		w.WriteHeader(http.StatusBadGateway)
		fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
		fifos.LogNonOK <- msg
		return false, nil
	}
	if 60 <= code && code <= 69 {
		w.Header().Add("Content-Type", "text/plain")
		w.WriteHeader(http.StatusUnauthorized)
		fmt.Fprintf(w, "%s\n%d (%s)\n", cols[1], code, codeName)
		fifos.LogNonOK <- msg
		return false, nil
	}
	if code < 20 || code > 29 {
		err = fmt.Errorf("unknown response code: %d", code)
		log.Printf("%s: %s\n", req.URL, err)
		fifos.LogNonOK <- msg
		return false, err
	}
	contentType, _, _ := strings.Cut(strings.TrimRight(cols[1], "\r\n"), ";")
	if contentType == ContentTypeGemini &&
		!strings.Contains(req.Header.Get("Accept"), ContentTypeGemini) {
		w.Header().Add("Content-Type", "text/html")
		w.WriteHeader(http.StatusOK)
		var raw []byte
		raw, err = io.ReadAll(br)
		if err != nil {
			log.Printf("%s: can not read response body: %+v\n", req.URL, err)
			return false, err
		}
		var buf bytes.Buffer
		fmt.Fprintf(&buf, `<!DOCTYPE html>
<html><head><title>%d (%s)</title></head><body>
`, code, codeName)
		pre := false
		for line := range strings.SplitSeq(string(raw), "\n") {
			if strings.HasPrefix(line, "```") {
				if pre {
					buf.WriteString("</pre>\n")
				} else {
					buf.WriteString("<pre>")
					buf.WriteString(line[3:])
					buf.WriteString("\n")
				}
				pre = !pre
				continue
			}
			if pre {
				fmt.Fprintf(&buf, "%s\n", line)
				continue
			}
			if strings.HasPrefix(line, "=>") {
				line = strings.TrimLeft(line[2:], " ")
				cols = strings.Fields(line)
				u1 := geminifyURL(host, cols[0], paths...)
				u2 := geminifyURL(host, cols[0])
				switch len(cols) {
				case 1:
					fmt.Fprintf(
						&buf, "<a href=\"%s\">%s</a>",
						u1, html.EscapeString(cols[0]),
					)
					if u1 != u2 {
						fmt.Fprintf(&buf, "&nbsp;<a href=\"%s\">[2]</a>", u2)
					}
					fmt.Fprintf(&buf, "<br/>\n")
				default:
					fmt.Fprintf(
						&buf, "<a href=\"%s\">%s</a>",
						u1, html.EscapeString(strings.Join(cols[1:], " ")),
					)
					if u1 != u2 {
						fmt.Fprintf(&buf, "&nbsp;<a href=\"%s\">[2]</a>", u2)
					}
					fmt.Fprintf(&buf, "(<tt>%s</tt>)<br/>\n", cols[0])
				}
				continue
			}
			if strings.HasPrefix(line, "# ") {
				fmt.Fprintf(&buf, "<h1>%s</h1>\n", html.EscapeString(line[2:]))
				continue
			}
			if strings.HasPrefix(line, "## ") {
				fmt.Fprintf(&buf, "<h2>%s</h2>\n", html.EscapeString(line[3:]))
				continue
			}
			if strings.HasPrefix(line, "### ") {
				fmt.Fprintf(&buf, "<h3>%s</h3>\n", html.EscapeString(line[4:]))
				continue
			}
			if strings.HasPrefix(line, "> ") {
				fmt.Fprintf(
					&buf, "<blockquote><tt>%s</tt></blockquote>\n",
					html.EscapeString(line[2:]),
				)
				continue
			}
			fmt.Fprintf(&buf, "%s<br/>\n", html.EscapeString(line))
		}
		buf.WriteString("</body></html>\n")
		_, err = w.Write(buf.Bytes())
		fifos.LogOK <- msg
		return false, err
	}
	w.Header().Add("Content-Type", contentType)
	w.WriteHeader(http.StatusOK)
	_, err = io.Copy(w, br)
	if err != nil {
		log.Printf("%s: can not read response body: %+v\n", req.URL, err)
	}
	fifos.LogOK <- msg
	return false, err
}
