import type { AgentSessionRecord } from "../repositories/agents.js";

export class TaskerAdapterNotConfiguredError extends Error {
  constructor() {
    super("NODEDC_INTERNAL_ACCESS_TOKEN is required for Tasker adapter calls.");
    this.name = "TaskerAdapterNotConfiguredError";
  }
}

export class TaskerAdapterError extends Error {
  constructor(
    message: string,
    readonly statusCode: number,
    readonly payload: unknown
  ) {
    super(message);
    this.name = "TaskerAdapterError";
  }
}

export class TaskerAdapterUnavailableError extends Error {
  constructor(readonly causeError: unknown) {
    super("Tasker internal adapter is unavailable.");
    this.name = "TaskerAdapterUnavailableError";
  }
}

export type TaskerClientConfig = {
  baseUrl: string;
  internalAccessToken?: string;
};

export type TaskerAgentContext = {
  agentId: string;
  ownerUserId: string;
  tokenId: string;
};

export type GrantedProjectInput = {
  workspace_slug: string;
  project_id: string | null;
  mode: string;
  scopes: string[];
};

export type ListGrantedProjectsInput = {
  grants: GrantedProjectInput[];
};

export type CreateIssueInput = {
  project_id: string;
  workspace_slug?: string | null;
  title: string;
  description?: string;
  priority?: "none" | "low" | "medium" | "high" | "urgent";
  structured_blocks?: unknown[];
};

export type CreateProjectInput = {
  workspace_slug: string;
  name: string;
  identifier?: string;
  description?: string;
};

export type GetIssueInput = {
  project_id: string;
  workspace_slug?: string | null;
};

export type UpdateIssueInput = {
  project_id: string;
  workspace_slug?: string | null;
  title?: string;
  description?: string;
  priority?: "none" | "low" | "medium" | "high" | "urgent";
  structured_blocks?: unknown[];
};

export type MoveIssueInput = {
  project_id: string;
  workspace_slug?: string | null;
  state_id: string;
};

export type CommentInput = {
  project_id: string;
  workspace_slug?: string | null;
  body: string;
};

export type AttachFileInput = {
  project_id: string;
  workspace_slug?: string | null;
  file_name: string;
  mime_type: string;
  content_base64: string;
};

export type SetLabelsInput = {
  project_id: string;
  workspace_slug?: string | null;
  label_ids: string[];
};

export type EnsureLabelsInput = {
  project_id: string;
  workspace_slug?: string | null;
  labels: Array<{
    name: string;
    color?: string;
  }>;
};

export type AssignIssueInput = {
  project_id: string;
  workspace_slug?: string | null;
  member_ids: string[];
};

export class TaskerClient {
  constructor(private readonly config: TaskerClientConfig) {}

  async listGrantedProjects(session: AgentSessionRecord): Promise<unknown> {
    return this.request("/api/internal/nodedc/agent/projects/resolve", {
      method: "POST",
      session,
      body: {
        grants: session.grants.map((grant) => ({
          workspace_slug: grant.workspaceSlug,
          project_id: grant.projectId,
          mode: grant.mode,
          scopes: grant.scopes,
        })),
      } satisfies ListGrantedProjectsInput,
    });
  }

  async getProjectContext(session: AgentSessionRecord, projectId: string, workspaceSlug?: string | null): Promise<unknown> {
    const searchParams = new URLSearchParams();

    if (workspaceSlug) {
      searchParams.set("workspace_slug", workspaceSlug);
    }

    return this.request(`/api/internal/nodedc/agent/projects/${encodeURIComponent(projectId)}/context?${searchParams.toString()}`, {
      method: "GET",
      session,
    });
  }

  async createProject(session: AgentSessionRecord, input: CreateProjectInput): Promise<unknown> {
    return this.request("/api/internal/nodedc/agent/projects", {
      method: "POST",
      session,
      body: input,
    });
  }

  async listIssues(session: AgentSessionRecord, projectId: string, workspaceSlug?: string | null, query?: string): Promise<unknown> {
    const searchParams = new URLSearchParams({ project_id: projectId });

    if (workspaceSlug) {
      searchParams.set("workspace_slug", workspaceSlug);
    }

    if (query) {
      searchParams.set("query", query);
    }

    return this.request(`/api/internal/nodedc/agent/issues?${searchParams.toString()}`, {
      method: "GET",
      session,
    });
  }

  async getIssue(session: AgentSessionRecord, issueId: string, input: GetIssueInput): Promise<unknown> {
    const searchParams = new URLSearchParams({ project_id: input.project_id });
    if (input.workspace_slug) {
      searchParams.set("workspace_slug", input.workspace_slug);
    }
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}?${searchParams.toString()}`, {
      method: "GET",
      session,
    });
  }

  async createIssue(session: AgentSessionRecord, input: CreateIssueInput): Promise<unknown> {
    return this.request("/api/internal/nodedc/agent/issues", {
      method: "POST",
      session,
      body: input,
    });
  }

  async updateIssue(session: AgentSessionRecord, issueId: string, input: UpdateIssueInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}`, {
      method: "PATCH",
      session,
      body: input,
    });
  }

  async moveIssue(session: AgentSessionRecord, issueId: string, input: MoveIssueInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/move`, {
      method: "POST",
      session,
      body: input,
    });
  }

  async appendComment(session: AgentSessionRecord, issueId: string, input: CommentInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/comments`, {
      method: "POST",
      session,
      body: input,
    });
  }

  async attachFile(session: AgentSessionRecord, issueId: string, input: AttachFileInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/attachments`, {
      method: "POST",
      session,
      body: input,
    });
  }

  async setLabels(session: AgentSessionRecord, issueId: string, input: SetLabelsInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/labels`, {
      method: "PUT",
      session,
      body: input,
    });
  }

  async ensureLabels(session: AgentSessionRecord, input: EnsureLabelsInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/projects/${encodeURIComponent(input.project_id)}/labels/ensure`, {
      method: "POST",
      session,
      body: input,
    });
  }

  async assignIssue(session: AgentSessionRecord, issueId: string, input: AssignIssueInput): Promise<unknown> {
    return this.request(`/api/internal/nodedc/agent/issues/${encodeURIComponent(issueId)}/assignees`, {
      method: "PUT",
      session,
      body: input,
    });
  }

  private async request(
    path: string,
    input: {
      method: "GET" | "POST" | "PATCH" | "PUT";
      session: AgentSessionRecord;
      body?: unknown;
    }
  ): Promise<unknown> {
    if (!this.config.internalAccessToken) {
      throw new TaskerAdapterNotConfiguredError();
    }

    const response = await this.fetchTasker(path, input);

    const payload = await readResponsePayload(response);

    if (!response.ok) {
      throw new TaskerAdapterError("Tasker internal adapter request failed.", response.status, payload);
    }

    return payload;
  }

  private async fetchTasker(
    path: string,
    input: {
      method: "GET" | "POST" | "PATCH" | "PUT";
      session: AgentSessionRecord;
      body?: unknown;
    }
  ): Promise<Response> {
    try {
      const requestBody = attachAgentMetadata(input.session, input.body);
      return await fetch(new URL(path, this.config.baseUrl), {
        method: input.method,
        headers: {
          Authorization: `Bearer ${this.config.internalAccessToken}`,
          "Content-Type": "application/json",
          "X-NODEDC-Agent-Id": input.session.agent.id,
          "X-NODEDC-Agent-Owner-User-Id": input.session.agent.ownerUserId,
          "X-NODEDC-Agent-Token-Id": input.session.token.id,
        },
        body: requestBody === undefined ? undefined : JSON.stringify(requestBody),
      });
    } catch (error) {
      throw new TaskerAdapterUnavailableError(error);
    }
  }
}

function attachAgentMetadata(session: AgentSessionRecord, body: unknown): unknown {
  if (body === undefined || !isPlainRecord(body)) {
    return body;
  }

  return {
    ...body,
    _agent: {
      display_name: session.agent.displayName,
      avatar_url: session.agent.avatarUrl,
    },
  };
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function readResponsePayload(response: Response): Promise<unknown> {
  const text = await response.text();

  if (!text) {
    return null;
  }

  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}
