package ai.decompile.ai.controller;

import ai.decompile.ai.model.dto.ChatRequest;
import ai.decompile.ai.model.dto.MessageResponse;
import ai.decompile.ai.model.dto.SessionResponse;
import ai.decompile.ai.model.dto.StreamEvent;
import ai.decompile.ai.service.ChatService;
import ai.decompile.ai.service.ChatSessionService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequiredArgsConstructor
@ConditionalOnProperty(name = "app.ai.enabled", havingValue = "true", matchIfMissing = true)
@Tag(name = "Chat", description = "AI-powered chat for binary analysis")
public class ChatController {
  private final ChatService chatService;
  private final ChatSessionService sessionService;
  private final ObjectMapper objectMapper;

  @PostMapping("/binaries/{binaryId}/chat/sessions")
  @Operation(summary = "Create a new chat session for a binary")
  public ResponseEntity<SessionResponse> createSession(
      @PathVariable UUID binaryId,
      @RequestParam(required = false) UUID projectId,
      @RequestParam(required = false, defaultValue = "gemma4:12b") String model) {
    if (projectId == null) {
      return ResponseEntity.badRequest().build();
    }
    var session = sessionService.createSession(binaryId, projectId, model);
    return ResponseEntity.status(HttpStatus.CREATED).body(SessionResponse.from(session));
  }

  @GetMapping("/binaries/{binaryId}/chat/sessions")
  @Operation(summary = "List chat sessions for a binary")
  public ResponseEntity<List<SessionResponse>> listSessions(@PathVariable UUID binaryId) {
    var sessions = sessionService.getSessions(binaryId);
    return ResponseEntity.ok(sessions.stream().map(SessionResponse::from).toList());
  }

  @GetMapping("/chat/sessions/{sessionId}")
  @Operation(summary = "Get chat session details")
  public ResponseEntity<SessionResponse> getSession(@PathVariable UUID sessionId) {
    var session = sessionService.getSession(sessionId);
    return ResponseEntity.ok(SessionResponse.from(session));
  }

  @DeleteMapping("/chat/sessions/{sessionId}")
  @Operation(summary = "Delete a chat session and all its messages")
  public ResponseEntity<Void> deleteSession(@PathVariable UUID sessionId) {
    sessionService.deleteSession(sessionId);
    return ResponseEntity.noContent().build();
  }

  @GetMapping("/chat/sessions/{sessionId}/messages")
  @Operation(summary = "Get message history for a chat session")
  public ResponseEntity<List<MessageResponse>> getMessages(@PathVariable UUID sessionId) {
    var messages = sessionService.getMessages(sessionId);
    return ResponseEntity.ok(messages.stream().map(MessageResponse::from).toList());
  }

  @Hidden
  @PostMapping(
      value = "/chat/sessions/{sessionId}/messages",
      produces = MediaType.TEXT_EVENT_STREAM_VALUE)
  public Flux<ServerSentEvent<String>> sendMessage(
      @PathVariable UUID sessionId, @Valid @RequestBody ChatRequest request) {

    return chatService
        .chat(sessionId, request.content())
        .map(this::toSseEvent)
        .onErrorResume(e -> Flux.just(errorEvent(e)));
  }

  private ServerSentEvent<String> toSseEvent(StreamEvent event) {
    var data =
        switch (event) {
          case StreamEvent.Chunk(var content) ->
              Map.of("type", "chunk", "content", (Object) content);
          case StreamEvent.Thinking(var message) ->
              Map.of("type", "thinking", "message", (Object) message);
          case StreamEvent.ToolCallStart(var tool) ->
              Map.of("type", "tool_call_start", "tool", (Object) tool);
          case StreamEvent.ToolCallEnd(var tool, var durationMs, var resultSummary) ->
              Map.of(
                  "type",
                  "tool_call_end",
                  "tool",
                  tool,
                  "durationMs",
                  durationMs,
                  "resultSummary",
                  (Object) resultSummary);
          case StreamEvent.Done(var sessionId) ->
              Map.of("type", "done", "sessionId", (Object) sessionId);
          case StreamEvent.Error(var message) ->
              Map.of("type", "error", "message", (Object) message);
        };
    return ServerSentEvent.<String>builder().data(toJson(data)).build();
  }

  private ServerSentEvent<String> errorEvent(Throwable e) {
    var msg = Objects.nonNull(e.getMessage()) ? e.getMessage() : "Unknown error";
    return ServerSentEvent.<String>builder()
        .data(toJson(Map.of("type", "error", "message", msg)))
        .build();
  }

  private String toJson(Object obj) {
    try {
      return objectMapper.writeValueAsString(obj);
    } catch (JsonProcessingException e) {
      return "{\"type\":\"error\",\"message\":\"JSON serialization failed\"}";
    }
  }
}
