import axios from "axios";
import { openHands } from "../open-hands-axios";
import { ConversationTrigger, GetVSCodeUrlResponse } from "../open-hands.types";
import { Provider } from "#/types/settings";
import { SuggestedTask } from "#/utils/types";
import { buildHttpBaseUrl } from "#/utils/websocket-url";
import { buildSessionHeaders } from "#/utils/utils";
import type {
  V1SendMessageRequest,
  V1SendMessageResponse,
  V1AppConversationStartRequest,
  V1AppConversationStartTask,
  V1AppConversationStartTaskPage,
  V1AppConversation,
  V1AppConversationPage,
  GetSkillsResponse,
  GetHooksResponse,
  V1RuntimeConversationInfo,
  PluginSpec,
} from "./v1-conversation-service.types";

class V1ConversationService {
  /**
   * Build the full URL for V1 runtime-specific endpoints
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param path The API path (e.g., "/api/vscode/url")
   * @returns Full URL to the runtime endpoint
   */
  private static buildRuntimeUrl(
    conversationUrl: string | null | undefined,
    path: string,
  ): string {
    const baseUrl = buildHttpBaseUrl(conversationUrl);
    return `${baseUrl}${path}`;
  }

  /**
   * Send a message to a V1 conversation
   * @param conversationId The conversation ID
   * @param message The message to send
   * @returns The sent message response
   */
  static async sendMessage(
    conversationId: string,
    message: V1SendMessageRequest,
  ): Promise<V1SendMessageResponse> {
    const { data } = await openHands.post<V1SendMessageResponse>(
      `/api/conversations/${conversationId}/events`,
      message,
    );

    return data;
  }

  /**
   * Create a new V1 conversation using the app-conversations API
   * Returns the start task immediately with app_conversation_id as null.
   * You must poll getStartTask() until status is READY to get the conversation ID.
   *
   * @returns AppConversationStartTask with task ID
   */
  static async createConversation(
    selectedRepository?: string,
    git_provider?: Provider,
    initialUserMsg?: string,
    selected_branch?: string,
    conversationInstructions?: string,
    suggestedTask?: SuggestedTask,
    trigger?: ConversationTrigger,
    parent_conversation_id?: string,
    agent_type?: "default" | "plan",
    plugins?: PluginSpec[],
    sandbox_id?: string,
    llm_model?: string,
  ): Promise<V1AppConversationStartTask> {
    const body: V1AppConversationStartRequest = {
      selected_repository: selectedRepository,
      git_provider,
      selected_branch,
      suggested_task: suggestedTask,
      title: conversationInstructions,
      trigger,
      parent_conversation_id: parent_conversation_id || null,
      agent_type,
      plugins: plugins || null,
      sandbox_id: sandbox_id || null,
      llm_model: llm_model || null,
    };

    // suggested_task implies the backend will construct the initial_message
    if (!suggestedTask && initialUserMsg) {
      body.initial_message = {
        role: "user",
        content: [
          {
            type: "text",
            text: initialUserMsg,
          },
        ],
      };
    }

    const { data } = await openHands.post<V1AppConversationStartTask>(
      "/api/v1/app-conversations",
      body,
    );

    return data;
  }

  /**
   * Get a start task by ID
   * Poll this endpoint until status is READY to get the app_conversation_id
   *
   * @param taskId The task UUID
   * @returns AppConversationStartTask or null
   */
  static async getStartTask(
    taskId: string,
  ): Promise<V1AppConversationStartTask | null> {
    const { data } = await openHands.get<(V1AppConversationStartTask | null)[]>(
      `/api/v1/app-conversations/start-tasks?ids=${taskId}`,
    );

    return data[0] || null;
  }

  /**
   * Search for start tasks (ongoing tasks that haven't completed yet)
   * Use this to find tasks that were started but the user navigated away
   *
   * Note: Backend supports filtering by limit and created_at__gte. To filter by repository/trigger,
   * filter the results client-side after fetching.
   *
   * @param limit Maximum number of tasks to return (max 100)
   * @returns Array of start tasks from the last 20 minutes
   */
  static async searchStartTasks(
    limit: number = 100,
  ): Promise<V1AppConversationStartTask[]> {
    const params = new URLSearchParams();
    params.append("limit", limit.toString());

    // Only get tasks from the last 20 minutes
    const twentyMinutesAgo = new Date(Date.now() - 20 * 60 * 1000);
    params.append("created_at__gte", twentyMinutesAgo.toISOString());

    const { data } = await openHands.get<V1AppConversationStartTaskPage>(
      `/api/v1/app-conversations/start-tasks/search?${params.toString()}`,
    );

    return data.items;
  }

  /**
   * Get the VSCode URL for a V1 conversation
   * Uses the custom runtime URL from the conversation
   * Note: V1 endpoint doesn't require conversationId in the URL path - it's identified via session API key header
   *
   * @param _conversationId The conversation ID (not used in V1, kept for interface compatibility)
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param sessionApiKey Session API key for authentication (required for V1)
   * @returns VSCode URL response
   */
  static async getVSCodeUrl(
    _conversationId: string,
    conversationUrl: string | null | undefined,
    sessionApiKey?: string | null,
  ): Promise<GetVSCodeUrlResponse> {
    const url = this.buildRuntimeUrl(conversationUrl, "/api/vscode/url");
    const headers = buildSessionHeaders(sessionApiKey);

    // V1 API returns {url: '...'} instead of {vscode_url: '...'}
    // Map it to match the expected interface
    const { data } = await axios.get<{ url: string | null }>(url, { headers });
    return {
      vscode_url: data.url,
    };
  }

  /**
   * Pause a V1 conversation
   * Uses the custom runtime URL from the conversation
   *
   * @param conversationId The conversation ID
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param sessionApiKey Session API key for authentication (required for V1)
   * @returns Success response
   */
  static async pauseConversation(
    conversationId: string,
    conversationUrl: string | null | undefined,
    sessionApiKey?: string | null,
  ): Promise<{ success: boolean }> {
    const url = this.buildRuntimeUrl(
      conversationUrl,
      `/api/conversations/${conversationId}/pause`,
    );
    const headers = buildSessionHeaders(sessionApiKey);

    const { data } = await axios.post<{ success: boolean }>(
      url,
      {},
      { headers },
    );
    return data;
  }

  /**
   * Ask the agent a side question without queueing a full turn.
   * @param conversationId The conversation ID
   * @param conversationUrl The conversation URL
   * @param question The side question to ask
   * @param sessionApiKey Session API key for authentication
   * @returns The agent's response
   */
  static async askAgent(
    conversationId: string,
    conversationUrl: string | null | undefined,
    question: string,
    sessionApiKey?: string | null,
  ): Promise<{ response: string }> {
    const url = this.buildRuntimeUrl(
      conversationUrl,
      `/api/conversations/${conversationId}/ask_agent`,
    );
    const headers = buildSessionHeaders(sessionApiKey);

    const { data } = await axios.post<{ response: string }>(
      url,
      { question },
      { headers },
    );
    return data;
  }

  /**
   * Switch the running conversation's LLM to a saved profile.
   * Goes through the app-server proxy, which loads the profile from user
   * settings (with api_key) and hands the LLM to the agent-server's
   * `switch_llm` endpoint.
   */
  static async switchProfile(
    conversationId: string,
    profileName: string,
  ): Promise<void> {
    await openHands.post(
      `/api/v1/app-conversations/${conversationId}/switch_profile`,
      { profile_name: profileName },
    );
  }

  /**
   * Resume a V1 conversation
   * Uses the custom runtime URL from the conversation
   *
   * @param conversationId The conversation ID
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param sessionApiKey Session API key for authentication (required for V1)
   * @returns Success response
   */
  static async resumeConversation(
    conversationId: string,
    conversationUrl: string | null | undefined,
    sessionApiKey?: string | null,
  ): Promise<{ success: boolean }> {
    const url = this.buildRuntimeUrl(
      conversationUrl,
      `/api/conversations/${conversationId}/run`,
    );
    const headers = buildSessionHeaders(sessionApiKey);

    const { data } = await axios.post<{ success: boolean }>(
      url,
      {},
      { headers },
    );
    return data;
  }

  /**
   * Batch get V1 app conversations by their IDs
   * Returns null for any missing conversations
   *
   * @param ids Array of conversation IDs (max 100)
   * @returns Array of conversations or null for missing ones
   */
  static async batchGetAppConversations(
    ids: string[],
  ): Promise<(V1AppConversation | null)[]> {
    if (ids.length === 0) {
      return [];
    }
    if (ids.length > 100) {
      throw new Error("Cannot request more than 100 conversations at once");
    }

    const params = new URLSearchParams();
    ids.forEach((id) => params.append("ids", id));

    const { data } = await openHands.get<(V1AppConversation | null)[]>(
      `/api/v1/app-conversations?${params.toString()}`,
    );
    return data;
  }

  /**
   * Upload a single file to the V1 conversation workspace
   * V1 API endpoint: POST /api/file/upload?path={path}
   *
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param sessionApiKey Session API key for authentication (required for V1)
   * @param file The file to upload
   * @param path The absolute path where the file should be uploaded (defaults to /workspace/{file.name})
   * @returns void on success, throws on error
   */
  static async uploadFile(
    conversationUrl: string | null | undefined,
    sessionApiKey: string | null | undefined,
    file: File,
    path?: string,
  ): Promise<void> {
    // Default to /workspace/{filename} if no path provided (must be absolute)
    const uploadPath = path || `/workspace/${file.name}`;
    const params = new URLSearchParams();
    params.append("path", uploadPath);
    const url = this.buildRuntimeUrl(
      conversationUrl,
      `/api/file/upload?${params.toString()}`,
    );
    const headers = buildSessionHeaders(sessionApiKey);

    // Create FormData with the file
    const formData = new FormData();
    formData.append("file", file);

    // Upload file
    await axios.post(url, formData, {
      headers: {
        ...headers,
        "Content-Type": "multipart/form-data",
      },
    });
  }

  /**
   * Get the conversation config (runtime_id) for a V1 conversation
   * @param conversationId The conversation ID
   * @returns Object containing runtime_id
   */
  static async getConversationConfig(
    conversationId: string,
  ): Promise<{ runtime_id: string }> {
    const url = `/api/conversations/${conversationId}/config`;
    const { data } = await openHands.get<{ runtime_id: string }>(url);
    return data;
  }

  /**
   * Update a V1 conversation's public flag
   * @param conversationId The conversation ID
   * @param isPublic Whether the conversation should be public
   * @returns Updated conversation info
   */
  static async updateConversationPublicFlag(
    conversationId: string,
    isPublic: boolean,
  ): Promise<V1AppConversation> {
    const { data } = await openHands.patch<V1AppConversation>(
      `/api/v1/app-conversations/${conversationId}`,
      { public: isPublic },
    );
    return data;
  }

  /**
   * Update a V1 conversation's repository settings
   * @param conversationId The conversation ID
   * @param repository The repository to attach (e.g., "owner/repo") or null to remove
   * @param branch The branch to use (optional)
   * @param gitProvider The git provider (e.g., "github", "gitlab")
   * @returns Updated conversation info
   */
  static async updateConversationRepository(
    conversationId: string,
    repository: string | null,
    branch?: string | null,
    gitProvider?: string | null,
  ): Promise<V1AppConversation> {
    const payload: Record<string, string | null | undefined> = {};

    if (repository !== undefined) {
      payload.selected_repository = repository;
    }
    if (branch !== undefined) {
      payload.selected_branch = branch;
    }
    if (gitProvider !== undefined) {
      payload.git_provider = gitProvider;
    }

    const { data } = await openHands.patch<V1AppConversation>(
      `/api/v1/app-conversations/${conversationId}`,
      payload,
    );
    return data;
  }

  /**
   * Read a file from a specific conversation's sandbox workspace
   * @param conversationId The conversation ID
   * @param filePath Path to the file to read within the sandbox workspace (defaults to /workspace/project/.agents_tmp/PLAN.md)
   * @returns The content of the file or an empty string if the file doesn't exist
   */
  static async readConversationFile(
    conversationId: string,
    filePath: string = "/workspace/project/.agents_tmp/PLAN.md",
  ): Promise<string> {
    const params = new URLSearchParams();
    params.append("file_path", filePath);

    const { data } = await openHands.get<string>(
      `/api/v1/app-conversations/${conversationId}/file?${params.toString()}`,
    );
    return data;
  }

  /**
   * Download a conversation trajectory as a zip file
   * @param conversationId The conversation ID
   * @returns A blob containing the zip file
   */
  static async downloadConversation(conversationId: string): Promise<Blob> {
    const response = await openHands.get(
      `/api/v1/app-conversations/${conversationId}/download`,
      {
        responseType: "blob",
      },
    );
    return response.data;
  }

  /**
   * Get all skills associated with a V1 conversation
   * @param conversationId The conversation ID
   * @returns The available skills associated with the conversation
   */
  static async getSkills(conversationId: string): Promise<GetSkillsResponse> {
    const { data } = await openHands.get<GetSkillsResponse>(
      `/api/v1/app-conversations/${conversationId}/skills`,
    );
    return data;
  }

  /**
   * Get all hooks associated with a V1 conversation
   * @param conversationId The conversation ID
   * @returns The available hooks associated with the conversation
   */
  static async getHooks(conversationId: string): Promise<GetHooksResponse> {
    const { data } = await openHands.get<GetHooksResponse>(
      `/api/v1/app-conversations/${conversationId}/hooks`,
    );
    return data;
  }

  /**
   * Get conversation info directly from the runtime for a V1 conversation
   * Uses the custom runtime URL from the conversation
   *
   * @param conversationId The conversation ID
   * @param conversationUrl The conversation URL (e.g., "http://localhost:54928/api/conversations/...")
   * @param sessionApiKey Session API key for authentication (required for V1)
   * @returns Conversation info from the runtime
   */
  static async getRuntimeConversation(
    conversationId: string,
    conversationUrl: string | null | undefined,
    sessionApiKey?: string | null,
  ): Promise<V1RuntimeConversationInfo> {
    // The agent-server provides a full ``conversationUrl`` with a
    // ``/api/conversations/{id}`` path (the SDK unified the LLM and ACP
    // endpoints onto this single route). We still preserve the URL's path
    // verbatim because a proxy deployment may add a prefix (e.g.
    // ``/runtime/55313/api/conversations/...``). If the URL is missing or
    // malformed we fall back to the default path derived from
    // ``window.location``.
    //
    // Either way we route through ``buildRuntimeUrl`` so its
    // ``extractBaseHost`` rewrites localhost/127.0.0.1 to the browser's
    // hostname when accessed from a non-local browser (proxy/external host
    // deployments). Without this, a conversation_url containing
    // ``localhost`` is unreachable from anywhere but the host machine.
    let path = `/api/conversations/${conversationId}`;
    if (conversationUrl) {
      try {
        path = new URL(conversationUrl).pathname;
      } catch {
        // Malformed URL — fall back to the default LLM path; buildRuntimeUrl
        // will resolve the host against window.location.
      }
    }
    const url = this.buildRuntimeUrl(conversationUrl, path);
    const headers = buildSessionHeaders(sessionApiKey);

    const { data } = await axios.get<V1RuntimeConversationInfo>(url, {
      headers,
    });
    return data;
  }

  /**
   * Search for V1 conversations by sandbox ID
   *
   * @param sandboxId The sandbox ID to filter by
   * @param limit Maximum number of results (default: 100)
   * @returns Array of conversations in the specified sandbox
   */
  static async searchConversationsBySandboxId(
    sandboxId: string,
    limit: number = 100,
  ): Promise<V1AppConversation[]> {
    const params = new URLSearchParams();
    params.append("sandbox_id__eq", sandboxId);
    params.append("limit", limit.toString());

    const { data } = await openHands.get<V1AppConversationPage>(
      `/api/v1/app-conversations/search?${params.toString()}`,
    );

    return data.items;
  }

  /**
   * Search for V1 conversations (general search with pagination)
   * Use this to populate the side menu with user's conversations
   *
   * @param limit Maximum number of results (default: 20)
   * @param pageId Optional page ID for pagination
   * @returns Paginated list of conversations
   */
  static async searchConversations(
    limit: number = 20,
    pageId?: string,
  ): Promise<V1AppConversationPage> {
    const params = new URLSearchParams();
    params.append("limit", limit.toString());
    if (pageId) {
      params.append("page_id", pageId);
    }

    const { data } = await openHands.get<V1AppConversationPage>(
      `/api/v1/app-conversations/search?${params.toString()}`,
    );

    return data;
  }

  /**
   * Delete a V1 conversation
   * @param conversationId The conversation ID to delete
   * @returns void on success
   */
  static async deleteConversation(conversationId: string): Promise<void> {
    await openHands.delete(`/api/v1/app-conversations/${conversationId}`);
  }

  /**
   * Update a V1 conversation's title
   * @param conversationId The conversation ID
   * @param title The new title
   * @returns Updated conversation info
   */
  static async updateConversationTitle(
    conversationId: string,
    title: string,
  ): Promise<V1AppConversation> {
    const { data } = await openHands.patch<V1AppConversation>(
      `/api/v1/app-conversations/${conversationId}`,
      { title },
    );
    return data;
  }
}

export default V1ConversationService;
