package proxy

import (
	"context"
	"errors"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"
)

type Director struct {
	Platform *url.URL
	AI       *url.URL
	Timeout  time.Duration
}

func New(platformURL, aiURL string, timeoutSec int) (*Director, error) {
	p, err := url.Parse(platformURL)
	if err != nil {
		return nil, err
	}
	a, err := url.Parse(aiURL)
	if err != nil {
		return nil, err
	}
	if timeoutSec <= 0 {
		timeoutSec = 180
	}
	return &Director{Platform: p, AI: a, Timeout: time.Duration(timeoutSec) * time.Second}, nil
}

// eofRetryTransport：空闲连接被 platform 关掉时首包 EOF，自动换新连接重试一次。
type eofRetryTransport struct {
	base http.RoundTripper
}

func (t eofRetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	base := t.base
	if base == nil {
		base = http.DefaultTransport
	}
	resp, err := base.RoundTrip(req)
	if err == nil || req.Context().Err() != nil {
		return resp, err
	}
	if !isTransientUpstreamEOF(err) {
		return resp, err
	}
	// Body 可能已部分读；仅在尚未发出/可读失败时安全重试 GET/有 GetBody 的请求
	if req.Body != nil && req.GetBody == nil && req.ContentLength != 0 {
		return resp, err
	}
	if req.GetBody != nil {
		body, gerr := req.GetBody()
		if gerr != nil {
			return resp, err
		}
		req = req.Clone(req.Context())
		req.Body = body
	}
	return base.RoundTrip(req)
}

func isTransientUpstreamEOF(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
		return true
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "eof") ||
		strings.Contains(msg, "connection reset") ||
		strings.Contains(msg, "broken pipe") ||
		strings.Contains(msg, "server closed idle connection")
}

func (d *Director) Handler() http.HandlerFunc {
	baseTransport := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   10 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext,
		MaxIdleConns:          100,
		MaxIdleConnsPerHost:   8,
		IdleConnTimeout:       30 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		ResponseHeaderTimeout: d.Timeout,
		// Windows 上 platform 重启后空闲连接易 EOF；关掉 keep-alive 比偶发 502 更稳
		DisableKeepAlives: true,
	}
	transport := eofRetryTransport{base: baseTransport}

	return func(w http.ResponseWriter, r *http.Request) {
		target, path := d.route(r.URL.Path)
		timeout := d.Timeout
		// dbsync agent push/pull：Windows SQLite 冷打开可超过默认超时
		if isDBSyncHeavyPath(path) && timeout < 180*time.Second {
			timeout = 180 * time.Second
		}

		proxy := httputil.NewSingleHostReverseProxy(target)
		proxy.Transport = transport
		proxy.FlushInterval = 100 * time.Millisecond
		proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) {
			rw.Header().Set("Content-Type", "application/json")
			msg := escape(err.Error())
			code := http.StatusBadGateway
			// 客户端/网关超时 → 503，便于 agent 按 retryable 重试（与 platform 语义一致）
			if errors.Is(err, context.DeadlineExceeded) || errors.Is(req.Context().Err(), context.DeadlineExceeded) ||
				strings.Contains(strings.ToLower(err.Error()), "deadline exceeded") ||
				strings.Contains(strings.ToLower(err.Error()), "context canceled") {
				code = http.StatusServiceUnavailable
			}
			rw.WriteHeader(code)
			_, _ = rw.Write([]byte(
				`{"code":` + strconv.Itoa(code) + `,"message":"upstream unavailable: ` + msg + `","retryable":true}`,
			))
		}
		proxy.ModifyResponse = func(resp *http.Response) error {
			for _, h := range []string{
				"Access-Control-Allow-Origin",
				"Access-Control-Allow-Credentials",
				"Access-Control-Allow-Headers",
				"Access-Control-Allow-Methods",
				"Access-Control-Expose-Headers",
				"Access-Control-Max-Age",
			} {
				resp.Header.Del(h)
			}
			resp.Header.Set("X-Gateway", "aijianzhan-gateway")
			return nil
		}

		r.URL.Path = path
		r.Host = target.Host
		r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
		r.Header.Set("X-Forwarded-Proto", "http")
		if r.Header.Get("X-Request-Id") == "" {
			r.Header.Set("X-Request-Id", newRequestID())
		}
		reqID := r.Header.Get("X-Request-Id")
		started := time.Now()
		rec := &statusRecorder{ResponseWriter: w, status: 200}

		ctx, cancel := context.WithTimeout(r.Context(), timeout)
		defer cancel()
		proxy.ServeHTTP(rec, r.WithContext(ctx))
		if isDBSyncHeavyPath(path) || rec.status >= 500 {
			log.Printf("gateway %s %s status=%d dur=%s req=%s", r.Method, path, rec.status, time.Since(started), reqID)
		}
	}
}

type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (s *statusRecorder) WriteHeader(code int) {
	s.status = code
	s.ResponseWriter.WriteHeader(code)
}

func isDBSyncHeavyPath(path string) bool {
	if strings.Contains(path, "/agent/sync/") {
		return true
	}
	if !strings.Contains(path, "/admin/sync/") {
		return false
	}
	return strings.Contains(path, "/push") ||
		strings.HasSuffix(path, "/reconcile") ||
		strings.HasSuffix(path, "/ingest") ||
		strings.HasSuffix(path, "/prepare") ||
		strings.HasSuffix(path, "/start") ||
		strings.HasSuffix(path, "/pull") ||
		strings.HasSuffix(path, "/bootstrap")
}

func (d *Director) route(path string) (*url.URL, string) {
	if strings.HasPrefix(path, "/ai/") || path == "/ai" {
		next := strings.TrimPrefix(path, "/ai")
		if next == "" {
			next = "/"
		}
		return d.AI, next
	}
	if path == "/api/v1/apps/generate" || strings.HasPrefix(path, "/api/v1/apps/generate?") {
		return d.AI, path
	}
	if strings.HasPrefix(path, "/api/v1/apps:generate") || strings.Contains(path, "/apps%3Agenerate") {
		return d.AI, "/api/v1/apps/generate"
	}
	return d.Platform, path
}

func escape(s string) string {
	s = strings.ReplaceAll(s, `\`, `\\`)
	s = strings.ReplaceAll(s, `"`, `\"`)
	if len(s) > 200 {
		s = s[:200]
	}
	return s
}

func newRequestID() string {
	return strings.ReplaceAll(time.Now().UTC().Format("20060102T150405.000000000"), ".", "")
}

// Health 聚合探测
func (d *Director) Health(w http.ResponseWriter, _ *http.Request) {
	client := &http.Client{Timeout: 2 * time.Second}
	out := []struct {
		Name string `json:"name"`
		OK   bool   `json:"ok"`
	}{
		probe(client, "platform", d.Platform.String()+"/api/v1/auth/login"),
		probe(client, "ai", d.AI.String()+"/health"),
	}
	allOK := true
	for _, s := range out {
		if !s.OK {
			allOK = false
			break
		}
	}
	code := http.StatusOK
	if !allOK {
		code = http.StatusServiceUnavailable
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_, _ = io.WriteString(w, `{"gateway":"ok","upstreams":[`)
	for i, s := range out {
		if i > 0 {
			_, _ = io.WriteString(w, ",")
		}
		ok := "false"
		if s.OK {
			ok = "true"
		}
		_, _ = io.WriteString(w, `{"name":"`+s.Name+`","ok":`+ok+`}`)
	}
	_, _ = io.WriteString(w, `]}`)
}

func probe(client *http.Client, name, rawURL string) struct {
	Name string `json:"name"`
	OK   bool   `json:"ok"`
} {
	req, _ := http.NewRequest(http.MethodGet, rawURL, nil)
	if name == "platform" {
		req, _ = http.NewRequest(http.MethodPost, rawURL, strings.NewReader(`{}`))
		req.Header.Set("Content-Type", "application/json")
	}
	resp, err := client.Do(req)
	if err != nil {
		return struct {
			Name string `json:"name"`
			OK   bool   `json:"ok"`
		}{Name: name, OK: false}
	}
	defer resp.Body.Close()
	return struct {
		Name string `json:"name"`
		OK   bool   `json:"ok"`
	}{Name: name, OK: true}
}
