package agent

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/consts"
)

const (
	openCodePermissionReplyOnce   = "once"
	openCodePermissionReplyReject = "reject"

	openCodePermissionRequestTimeout = 10 * time.Second
)

type openCodePermissionRequest struct {
	ID         string `json:"id"`
	SessionID  string `json:"sessionID"`
	Permission string `json:"permission"`
	Tool       *struct {
		MessageID string `json:"messageID"`
		CallID    string `json:"callID"`
	} `json:"tool,omitempty"`
}

func (d *OpenCodeDriver) handleOpenCodePermissionAsked(props json.RawMessage, currentSessionID string) {
	d.mu.Lock()
	generationRetiring := d.generationRetiring
	d.mu.Unlock()
	if generationRetiring {
		return
	}

	var request openCodePermissionRequest
	if err := json.Unmarshal(props, &request); err != nil {
		request.SessionID = strings.TrimSpace(currentSessionID)
		d.failOpenCodePermission(request, fmt.Errorf("decode OpenCode permission request: %w", err))
		return
	}
	request.ID = strings.TrimSpace(request.ID)
	request.SessionID = strings.TrimSpace(request.SessionID)
	request.Permission = strings.TrimSpace(request.Permission)
	if request.SessionID != strings.TrimSpace(currentSessionID) {
		diagnostic := openCodePermissionDiagnostic(request)
		diagnostic["current_session_id"] = strings.TrimSpace(currentSessionID)
		d.emitRuntimeDebug("opencode permission.asked ignored for another session", diagnostic)
		return
	}
	if request.ID == "" {
		d.failOpenCodePermission(request, fmt.Errorf("OpenCode permission request is missing an id"))
		return
	}

	d.mu.Lock()
	if d.generationRetiring {
		d.mu.Unlock()
		return
	}
	reply := d.permissionReply
	d.mu.Unlock()
	if reply != openCodePermissionReplyReject {
		reply = openCodePermissionReplyOnce
	}
	diagnostic := openCodePermissionDiagnostic(request)
	diagnostic["reply"] = reply
	d.emitRuntimeDebug("opencode permission.asked", diagnostic)

	ctx, cancel := context.WithTimeout(context.Background(), openCodePermissionRequestTimeout)
	defer cancel()
	if err := d.replyOpenCodePermission(ctx, request.ID, reply); err != nil {
		d.failOpenCodePermission(request, err)
	}
}

func (d *OpenCodeDriver) replyOpenCodePermission(ctx context.Context, requestID, reply string) error {
	body, _ := json.Marshal(map[string]string{"reply": reply})
	req, err := d.newRequest(ctx, http.MethodPost, fmt.Sprintf("/permission/%s/reply", requestID), strings.NewReader(string(body)))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	d.addDirectoryHeader(req, d.getWorkDir())

	resp, err := d.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("reply to OpenCode permission %s: %w", requestID, err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return fmt.Errorf("reply to OpenCode permission %s: status %d", requestID, resp.StatusCode)
	}
	return nil
}

func (d *OpenCodeDriver) failOpenCodePermission(request openCodePermissionRequest, replyErr error) {
	diagnostic := openCodePermissionDiagnostic(request)
	diagnostic["error"] = replyErr
	d.emitRuntimeDebug("opencode permission reply failed; stopping runtime", diagnostic)

	d.mu.Lock()
	if d.generationRetiring {
		d.mu.Unlock()
		return
	}
	lines := d.translatedLines
	serverCancel := d.beginOpenCodeGenerationRetirementLocked()
	d.mu.Unlock()
	if serverCancel != nil {
		serverCancel()
	}
	if lines != nil {
		data, _ := json.Marshal(translatedEvent{
			Kind:      consts.ParsedEventKindError,
			SessionID: request.SessionID,
			Message:   "OpenCode could not resolve a headless permission request; the runtime was stopped for recovery.",
		})
		lines <- string(data)
	}
}

func openCodePermissionDiagnostic(request openCodePermissionRequest) map[string]any {
	diagnostic := map[string]any{
		"session_id": request.SessionID,
		"request_id": request.ID,
		"permission": request.Permission,
	}
	if request.Tool != nil {
		diagnostic["tool_message_id"] = strings.TrimSpace(request.Tool.MessageID)
		diagnostic["tool_call_id"] = strings.TrimSpace(request.Tool.CallID)
	}
	return diagnostic
}
