import type { Logger } from "pino";
import { CURSOR_CLOUD_BASE_URL } from "@/core/constants.js";
import { AgentError, JobTimeoutError } from "@/core/errors.js";
import type {
  AgentStatus,
  AgentConversation,
  CursorAgent,
  FollowUpRequest,
  LaunchAgentRequest,
} from "@/types/index.js";

export class CursorCloudClient {
  private readonly baseUrl: string;
  private readonly authHeader: string;

  private readonly model: string;

  constructor(
    apiKey: string,
    model: string,
    private readonly logger: Logger,
  ) {
    this.baseUrl = CURSOR_CLOUD_BASE_URL;
    this.authHeader = "Basic " + Buffer.from(`${apiKey}:`).toString("base64");
    this.model = model;
  }

  async launchAgent(
    prompt: string,
    repoUrl: string,
    baseBranch: string,
    branchName: string,
    images?: Array<{ data: string; dimension: { width: number; height: number } }>,
  ): Promise<string> {
    const repoPath = this.extractRepoPath(repoUrl);

    const body: LaunchAgentRequest = {
      prompt: {
        text: prompt,
        images: images && images.length > 0 ? images : undefined,
      },
      model: this.model,
      source: {
        repository: repoPath,
        ref: baseBranch,
      },
      target: {
        branchName,
        autoCreatePr: true,
        openAsCursorGithubApp: false,
      },
    };

    this.logger.info(
      { repo: repoPath, branch: branchName, imageCount: images?.length ?? 0 },
      "agent_launched",
    );

    const agent = await this.request<CursorAgent>("POST", "/v0/agents", body);
    return agent.id;
  }

  async pollUntilDone(agentId: string, timeoutMs: number): Promise<AgentStatus> {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      const agent = await this.request<CursorAgent>("GET", `/v0/agents/${agentId}`);

      this.logger.debug({ agentId, status: agent.status }, "agent_polling");

      if (agent.status === "FINISHED" || agent.status === "FAILED" || agent.status === "STOPPED") {
        this.logger.info(
          { agentId, status: agent.status, summary: agent.summary },
          "agent_finished",
        );
        return agent.status;
      }

      await sleep(15_000);
    }

    throw new JobTimeoutError(`Agent ${agentId} did not finish within ${timeoutMs}ms`);
  }

  async sendFollowup(agentId: string, message: string): Promise<void> {
    const body: FollowUpRequest = {
      prompt: { text: message },
    };

    this.logger.info({ agentId }, "cursor_followup_sent");
    await this.request<{ id: string }>("POST", `/v0/agents/${agentId}/followup`, body);
  }

  async stopAgent(agentId: string): Promise<void> {
    this.logger.info({ agentId }, "Stopping Cursor agent");
    await this.request<{ id: string }>("POST", `/v0/agents/${agentId}/stop`);
  }

  async deleteAgent(agentId: string): Promise<void> {
    this.logger.info({ agentId }, "Deleting Cursor agent");
    await this.request<{ id: string }>("DELETE", `/v0/agents/${agentId}`);
  }

  async getConversation(agentId: string): Promise<string> {
    const conversation = await this.request<AgentConversation>(
      "GET",
      `/v0/agents/${agentId}/conversation`,
    );

    return conversation.messages
      .filter((m) => m.type === "assistant_message")
      .map((m) => m.text)
      .join("\n\n");
  }

  async getAgentDetails(agentId: string): Promise<CursorAgent> {
    return this.request<CursorAgent>("GET", `/v0/agents/${agentId}`);
  }

  async listModels(): Promise<{ models: string[] }> {
    return this.request<{ models: string[] }>("GET", "/v0/models");
  }

  /**
   * Lightweight auth probe: calls `/v0/models` and returns true when the API
   * key is accepted. Returns false on any error without logging (the AuthChecker
   * handles state-transition logging).
   */
  async checkAuth(): Promise<boolean> {
    try {
      await this.listModels();
      return true;
    } catch {
      return false;
    }
  }

  private extractRepoPath(repoUrl: string): string {
    const match = repoUrl.match(/github\.com[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
    if (!match?.[1]) {
      throw new AgentError(`Cannot extract owner/repo from URL: ${repoUrl}`, {
        isRetryable: false,
      });
    }
    return match[1];
  }

  private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    const url = `${this.baseUrl}${path}`;

    const headers: Record<string, string> = {
      Authorization: this.authHeader,
    };
    if (body) {
      headers["Content-Type"] = "application/json";
    }

    const response = await fetch(url, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      let errorMessage = `Cursor API ${method} ${path} failed: ${response.status}`;
      try {
        const data = (await response.json()) as Record<string, unknown>;
        if (data.message) errorMessage = String(data.message);
        else if (data.error) errorMessage = String(data.error);
      } catch {
        // ignore parse errors
      }

      const isRetryable = response.status === 429 || response.status >= 500;
      throw new AgentError(errorMessage, { isRetryable });
    }

    if (response.status === 204) {
      return {} as T;
    }

    return response.json() as Promise<T>;
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
