package dev_tool

// |@@| C

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"gardening/src/lib"
	"gardening/src/lib/config"
	"gardening/src/lib/contexter"
	erro "gardening/src/lib/error"
	"gardening/src/lib/filesystem"
	"gardening/src/lib/form"
	"gardening/src/lib/jsoner"
	"gardening/src/lib/logger"
	"gardening/src/lib/spy"
	"gitlab.com/ccyrillee/kitcla/goc"
	"gitlab.com/ccyrillee/kitcla/goc_debug"
	"io"
	"net"
	"net/http"
	"os"
	"strconv"
	"time"
)

type DevTool struct {
	Config     *config.Config
	Logger     *logger.Logger
	Contexter  *contexter.Contexter
	Jsoner     *jsoner.Jsoner
	Filesystem *filesystem.Filesystem
	Spy        *spy.Spy
	Data       struct {
		Port      string
		ProjectId string
		Available bool
		GocDebug  *goc_debug.Debugging
		Server    *http.Server
		Records   []*Record
	}
}

type Record struct {
	Uid       string        `json:"uid"`
	Timestamp time.Time     `json:"timestamp"`
	ParentUid string        `json:"parent_uid"`
	Logs      []*logger.Log `json:"logs"`
	RouteInfo *RouteInfo    `json:"route_info"`
	FormBag   *form.FormBag `json:"form_bag"`
}

type RouteInfo struct {
	Route       string
	RouteUrl    string
	RouteMethod string
	Mae         string
	MaeIn       string
	MaeOut      string
	Duration    string
	Params      map[string]string
}

func (this *DevTool) GetRecordByUid(ctx context.Context, uid string) *Record {
	return this.getOrCreateRecord(ctx, uid)
}

func (this *DevTool) FindCurrentRecord(ctx context.Context) (record *Record, has bool) {
	uid := this.Contexter.MayGetUid(ctx)
	if uid == "" {
		return nil, false
	}
	return this.GetRecordByUid(ctx, uid), true
}

func (this *DevTool) getOrCreateRecord(ctx context.Context, uid string) *Record {
	for _, record := range this.Data.Records {
		if record.Uid == uid {
			profile := this.Logger.GetProfileByUid(ctx, uid)
			record.Logs = profile.Logs
			return record
		}
	}
	record := &Record{
		Uid:       uid,
		RouteInfo: &RouteInfo{},
	}
	this.Data.Records = append(this.Data.Records, record)
	return record
}

func (this *DevTool) SendRecord(ctx context.Context) error {
	if !this.Config.IsDevEnv() {
		return nil
	}

	record := this.GetCurrentRecord(ctx)
	record.Timestamp = time.Now()
	_, err := this.sendRecord(ctx, "new-report", record)
	return err
}

func (this *DevTool) SendSpyReport(ctx context.Context, report *spy.Report) {
	_, err := this.sendRecord(ctx, "new-spy-report", report)
	lib.Poe(err)
}

func (this *DevTool) SendExpectationReport(ctx context.Context, report interface{}) string {
	id, err := this.sendRecord(ctx, "new-expectation-report", report)
	lib.Poe(err)
	return id
}

func (this *DevTool) GetDevToolUrl(path string) string {
	return "http://localhost:8077" + path
}

func (this *DevTool) GetExpectationUrl(id string) string {
	return this.GetDevToolUrl("/app/see-expectation?id=" + id)
}

func (this *DevTool) GetOpenGocXIdUrl() string {
	return this.GetDevToolUrl("/exchange/open-goc-x-id")
}

func (this *DevTool) sendRecord(ctx context.Context, endpoint string, payload interface{}) (string, error) {
	if !this.IsAvailable(ctx) {
		return "", nil
	}
	projectId := this.getSyncId(ctx)
	data := this.Jsoner.MustMarshal(payload)
	url := this.GetDevToolUrl("/exchange/" + endpoint + "?project_id=" + projectId)

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(data))
	if err != nil {
		return "", fmt.Errorf("cannot send the report to %s: %w", endpoint, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("received non-200 response: %d", resp.StatusCode)
	}
	b, err := io.ReadAll(resp.Body)
	var response struct {
		Id string `json:"id"`
	}
	this.Jsoner.MustUnmarshal(b, &response)

	return response.Id, nil
}

func (this *DevTool) SendSpyReportFromCtx(ctx context.Context) {
	report := this.Spy.GetReportFromCtx(ctx)
	this.SendSpyReport(ctx, report)
}

func (this *DevTool) GetCurrentRecord(ctx context.Context) *Record {
	return this.GetRecordByUid(ctx, this.Contexter.MayGetUid(ctx))
}

func (this *DevTool) RunDebugWsServer(ctx context.Context) {
	if !this.Config.IsDevEnv() {
		return
	}
	go this.startWsServer(ctx)
}

func (this *DevTool) startWsServer(ctx context.Context) {
	has := this.Config.HasEnvValue(ctx, "DEBUG_PORT")
	if has {
		this.setBindingPortFromEnvValue(ctx)
	} else {
		err := this.SetBindingPortFromRange(9620, 9650)
		lib.Poe(err)
	}
	this.ListenAndServe(ctx)
}

func (this *DevTool) setBindingPortFromEnvValue(ctx context.Context) {
	value := this.Config.GetEnvValue(ctx, "DEBUG_PORT")
	port, err := strconv.Atoi(value)
	if err != nil {
		panic(erro.N("Invalid DEBUG_PORT env value").KV("value", value))
	}
	has, err := this.setPortFromNumber(port, false)
	if !has {
		panic(erro.N("Invalid DEBUG_PORT env value : cannot bind").KV("value", value))
	}
	lib.Poe(err)
}

func (this *DevTool) GetBindingPort() string {
	return this.Data.Port
}

func (this *DevTool) SetBindingPortFromRange(startPort int, endPort int) (err error) {
	for port := startPort; port <= endPort; port++ {
		done, err := this.setPortFromNumber(port, true)
		if done {
			return nil
		}
		lib.Poe(err)
	}
	return erro.N("No available ports in the range").KV("start_port", startPort).KV("end_port", endPort)
}

func (this *DevTool) setPortFromNumber(port int, trying bool) (bool, error) {
	address := fmt.Sprintf("localhost:%d", port)
	listener, err := net.Listen("tcp", address)
	if err != nil {
		if trying {
			return false, err
		} else {
			return false, nil
		}
	}
	err = listener.Close()
	lib.Poe(err)
	bindingPort := strconv.Itoa(port)
	this.Data.Port = bindingPort
	return true, nil
}

func (this *DevTool) handleSSE(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", "*")

	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
		return
	}

	// Send initial connection message
	fmt.Fprintf(w, "data: Debug server connected\n\n")
	flusher.Flush()

	// Keep connection alive and send periodic heartbeat
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			fmt.Fprintf(w, "data: heartbeat\n\n")
			flusher.Flush()
		case <-r.Context().Done():
			return
		}
	}
}

func (this *DevTool) ListenAndServe(ctx context.Context) {
	bindingPort := this.GetBindingPort()
	mux := http.NewServeMux()
	mux.HandleFunc("/sse", this.handleSSE)
	mux.HandleFunc("/open-gix", this.OpenGix)

	this.Data.Server = &http.Server{
		Addr:    ":" + bindingPort,
		Handler: mux,
	}

	this.Logger.Info(ctx, "Dev tool SSE server started on :"+bindingPort)
	err := this.Data.Server.ListenAndServe()
	if err != nil && !errors.Is(err, http.ErrServerClosed) {
		lib.Poe(err)
	}
}

func (this *DevTool) Shutdown(ctx context.Context) error {
	if this.Data.Server == nil {
		return nil
	}

	// Create a context with timeout for shutdown to prevent hanging
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	err := this.Data.Server.Shutdown(shutdownCtx)
	if err != nil {
		return fmt.Errorf("failed to shutdown dev tool server: %w", err)
	}

	return nil
}

func (this *DevTool) OpenGix(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8077")
	w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
	w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
	if r.Method == http.MethodOptions {
		w.WriteHeader(http.StatusNoContent)
		return
	}
	id := r.URL.Query().Get("id")
	var debugItem goc.Debug
	for _, item := range this.Data.GocDebug.Items {
		if item.Id == id {
			debugItem = item
			break
		}
	}
	if debugItem.Id == "" {
		return
	}

	b, _ := json.Marshal(struct {
		ProjectId string `json:"project_id"`
		Path      string `json:"path"`
	}{
		ProjectId: this.getSyncId(context.Background()),
		Path:      debugItem.Path,
	})
	_, err := http.Post(this.GetOpenGocXIdUrl(), "application/json", bytes.NewBuffer(b))
	lib.Poe(err)
}

func (this *DevTool) GetUrlForLogReport(ctx context.Context) string {
	return this.GetDevToolUrl("/app/display-log-report?form[uid]=" + this.Contexter.MayGetUid(ctx))
}

func (this *DevTool) GetUrlForTaskCreator(ctx context.Context) string {
	return this.GetDevToolUrl("/app/new-task?form[sync_id]=" + this.getSyncId(ctx) + "&form[uid]=" + this.Contexter.MayGetUid(ctx))
}

func (this *DevTool) IsAvailable(ctx context.Context) bool {
	return this.Config.IsDevOrTestEnv()
}

func (this *DevTool) getSyncId(ctx context.Context) string {
	if this.Data.ProjectId != "" {
		return this.Data.ProjectId
	}

	devtFilePath := this.Config.GetRootDir() + "/coide.json"
	s, err := os.ReadFile(devtFilePath)
	lib.Poe(err)
	structure := &struct {
		SyncId string `json:"sync_id"`
	}{}
	this.Jsoner.MustUnmarshal(s, structure)
	this.Data.ProjectId = structure.SyncId
	return this.Data.ProjectId
}
