package proxy

import (
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	tea "charm.land/bubbletea/v2"
	"github.com/anotherhadi/spilltea/internal/config"
	"github.com/anotherhadi/spilltea/internal/intercept"
	"github.com/anotherhadi/spilltea/internal/plugins"
	goproxy "github.com/lqqyt2423/go-mitmproxy/proxy"
)

type ErrMsg struct{ Err error }

func StartCmd(broker *intercept.Broker, mgr *plugins.Manager) tea.Cmd {
	return func() tea.Msg {
		if err := Start(broker, mgr); err != nil {
			return ErrMsg{Err: err}
		}
		return ErrMsg{}
	}
}

type interceptAddon struct {
	goproxy.BaseAddon
	broker  *intercept.Broker
	plugins *plugins.Manager
}

// ClientConnected disables upstream cert fetching so the upstream TCP/TLS
// connection is established only after Hold() returns, not during CONNECT.
// Without this, the upstream connection sits idle while the TUI holds the
// request, and the server closes it (keep-alive timeout) → unexpected EOF.
func (a *interceptAddon) ClientConnected(clientConn *goproxy.ClientConn) {
	clientConn.UpstreamCert = false
}

func (a *interceptAddon) Request(f *goproxy.Flow) {
	if a.plugins != nil {
		switch a.plugins.RunSyncOnRequest(f) {
		case intercept.Drop:
			f.Response = dropResponse()
			return
		case intercept.Forward:
			go a.plugins.RunAsyncOnRequest(f)
			return
		}
	}

	if a.broker.Hold(f) == intercept.Drop {
		f.Response = dropResponse()
	}

	if a.plugins != nil {
		go a.plugins.RunAsyncOnRequest(f)
	}
}

func (a *interceptAddon) Response(f *goproxy.Flow) {
	if f.Response != nil {
		if len(f.Response.Body) == 0 && f.Response.BodyReader != nil {
			limit := int64(config.Global.App.MaxBodySizeMB) * 1024 * 1024
			body, err := io.ReadAll(io.LimitReader(f.Response.BodyReader, limit))
			if err != nil {
				log.Printf("proxy: reading response body: %v", err)
			}
			if int64(len(body)) == limit {
				log.Printf("proxy: response body truncated at %dMB for %s", config.Global.App.MaxBodySizeMB, f.Request.URL.Host)
				body = append(body, []byte(fmt.Sprintf("\n\n[body truncated at %dMB]", config.Global.App.MaxBodySizeMB))...)
			}
			f.Response.Body = body
			f.Response.BodyReader = nil
		}
		f.Response.ReplaceToDecodedBody()
	}

	if a.plugins != nil {
		switch a.plugins.RunSyncOnResponse(f) {
		case intercept.Drop:
			a.broker.SaveEntry(f)
			f.Response = dropResponse()
			go a.plugins.RunAsyncOnResponse(f)
			return
		case intercept.Forward:
			a.broker.SaveEntry(f)
			go a.plugins.RunAsyncOnResponse(f)
			return
		}
	}

	decision := a.broker.HoldResponse(f)
	a.broker.SaveEntry(f)
	if decision == intercept.Drop {
		f.Response = dropResponse()
	}

	if a.plugins != nil {
		go a.plugins.RunAsyncOnResponse(f)
	}
}

func Start(broker *intercept.Broker, mgr *plugins.Manager) error {
	cfg := config.Global.App
	addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
	caPath := config.ExpandPath(cfg.CertDir)

	if err := os.MkdirAll(caPath, 0o700); err != nil {
		return fmt.Errorf("ca dir: %w", err)
	}

	opts := &goproxy.Options{
		Addr:              addr,
		StreamLargeBodies: int64(cfg.MaxBodySizeMB) * 1024 * 1024,
		CaRootPath:        caPath,
		Upstream:          cfg.UpstreamProxy,
		SslInsecure:       cfg.SslInsecure,
	}

	p, err := goproxy.NewProxy(opts)
	if err != nil {
		return err
	}

	if cfg.ProxyAuth != "" {
		parts := strings.SplitN(cfg.ProxyAuth, ":", 2)
		if len(parts) == 2 {
			wantUser, wantPass := parts[0], parts[1]
			p.SetAuthProxy(func(res http.ResponseWriter, req *http.Request) (bool, error) {
				user, pass, ok := parseBasicProxyAuth(req.Header.Get("Proxy-Authorization"))
				userOK := subtle.ConstantTimeCompare([]byte(user), []byte(wantUser))
				passOK := subtle.ConstantTimeCompare([]byte(pass), []byte(wantPass))
				if !ok || userOK&passOK != 1 {
					res.Header().Set("Proxy-Authenticate", `Basic realm="spilltea"`)
					return false, fmt.Errorf("invalid credentials")
				}
				return true, nil
			})
		}
	}

	p.AddAddon(&interceptAddon{broker: broker, plugins: mgr})
	return p.Start()
}

func parseBasicProxyAuth(header string) (user, pass string, ok bool) {
	const prefix = "Basic "
	if !strings.HasPrefix(header, prefix) {
		return "", "", false
	}
	decoded, err := base64.StdEncoding.DecodeString(header[len(prefix):])
	if err != nil {
		return "", "", false
	}
	parts := strings.SplitN(string(decoded), ":", 2)
	if len(parts) != 2 {
		return "", "", false
	}
	return parts[0], parts[1], true
}

func dropResponse() *goproxy.Response {
	return &goproxy.Response{
		StatusCode: 502,
		Header:     http.Header{"Content-Type": []string{"text/plain"}},
		Body:       []byte("Dropped by spilltea"),
	}
}
