/**
 * HTTP client for emdash-core API.
 *
 * This client handles:
 * - Regular JSON API calls
 * - SSE streaming for agent chat
 * - Health checks
 */

import type {
  HealthResponse,
  SessionInfo,
  SearchResponse,
  IndexStatus,
  DbStats,
  AgentChatOptions,
  ImageData,
  Message,
  AuthLoginResponse,
  AuthPollResponse,
  AuthStatusResponse,
  TodosResponse,
} from './types.js';

function getMaxIterations(): number {
  return parseInt(process.env['EMDASH_MAX_ITERATIONS'] ?? '100', 10);
}

export class EmdashClient {
  private baseUrl: string;
  private abortController: AbortController | null = null;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Health
  // ─────────────────────────────────────────────────────────────────────────

  async health(): Promise<HealthResponse> {
    const response = await fetch(`${this.baseUrl}/api/health`, {
      signal: AbortSignal.timeout(5000),
    });
    if (!response.ok) {
      throw new Error(`Health check failed: ${response.status}`);
    }
    return response.json() as Promise<HealthResponse>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Agent Chat Streaming
  // ─────────────────────────────────────────────────────────────────────────

  async *agentChatStream(
    message: string,
    model?: string,
    sessionId?: string,
    maxIterations: number = getMaxIterations(),
    options?: AgentChatOptions,
    images?: ImageData[],
    history?: Message[]
  ): AsyncGenerator<string, void, unknown> {
    const requestOptions: AgentChatOptions = {
      max_iterations: maxIterations,
      verbose: true,
      ...options,
    };

    const payload: Record<string, unknown> = {
      message,
      options: requestOptions,
    };

    if (model) payload['model'] = model;
    if (sessionId) payload['session_id'] = sessionId;
    if (images) payload['images'] = images;
    if (history) payload['history'] = history;

    this.abortController = new AbortController();

    try {
      const response = await fetch(`${this.baseUrl}/api/agent/chat`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
        signal: this.abortController.signal,
      });

      if (!response.ok) {
        throw new Error(`Agent chat failed: ${response.status}`);
      }

      const reader = response.body?.getReader();
      if (!reader) {
        throw new Error('No response body');
      }

      const decoder = new TextDecoder();
      let buffer = '';

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() ?? '';

        for (const line of lines) {
          if (line.trim()) {
            yield line;
          }
        }
      }

      // Process remaining buffer
      if (buffer.trim()) {
        yield buffer;
      }
    } finally {
      this.abortController = null;
    }
  }

  async *agentContinueStream(
    sessionId: string,
    message: string,
    images?: ImageData[]
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = { message };
    if (images) payload['images'] = images;

    this.abortController = new AbortController();

    try {
      const response = await fetch(
        `${this.baseUrl}/api/agent/chat/${sessionId}/continue`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          signal: this.abortController.signal,
        }
      );

      if (!response.ok) {
        throw new Error(`Agent continue failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  async *planApproveStream(sessionId: string): AsyncGenerator<string, void, unknown> {
    this.abortController = new AbortController();

    try {
      const response = await fetch(
        `${this.baseUrl}/api/agent/chat/${sessionId}/plan/approve`,
        {
          method: 'POST',
          signal: this.abortController.signal,
        }
      );

      if (!response.ok) {
        throw new Error(`Plan approve failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  async *planRejectStream(
    sessionId: string,
    feedback: string = ''
  ): AsyncGenerator<string, void, unknown> {
    this.abortController = new AbortController();

    try {
      const url = new URL(`${this.baseUrl}/api/agent/chat/${sessionId}/plan/reject`);
      url.searchParams.set('feedback', feedback);

      const response = await fetch(url.toString(), {
        method: 'POST',
        signal: this.abortController.signal,
      });

      if (!response.ok) {
        throw new Error(`Plan reject failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  async *planmodeApproveStream(sessionId: string): AsyncGenerator<string, void, unknown> {
    this.abortController = new AbortController();

    try {
      const response = await fetch(
        `${this.baseUrl}/api/agent/chat/${sessionId}/planmode/approve`,
        {
          method: 'POST',
          signal: this.abortController.signal,
        }
      );

      if (!response.ok) {
        throw new Error(`Planmode approve failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  async *planmodeRejectStream(
    sessionId: string,
    feedback: string = ''
  ): AsyncGenerator<string, void, unknown> {
    this.abortController = new AbortController();

    try {
      const url = new URL(
        `${this.baseUrl}/api/agent/chat/${sessionId}/planmode/reject`
      );
      url.searchParams.set('feedback', feedback);

      const response = await fetch(url.toString(), {
        method: 'POST',
        signal: this.abortController.signal,
      });

      if (!response.ok) {
        throw new Error(`Planmode reject failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  async *clarificationAnswerStream(
    sessionId: string,
    answer: string
  ): AsyncGenerator<string, void, unknown> {
    this.abortController = new AbortController();

    try {
      const url = new URL(
        `${this.baseUrl}/api/agent/chat/${sessionId}/clarification/answer`
      );
      url.searchParams.set('answer', answer);

      const response = await fetch(url.toString(), {
        method: 'POST',
        signal: this.abortController.signal,
      });

      if (!response.ok) {
        throw new Error(`Clarification answer failed: ${response.status}`);
      }

      yield* this.readSSEStream(response);
    } finally {
      this.abortController = null;
    }
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Sessions
  // ─────────────────────────────────────────────────────────────────────────

  async listSessions(): Promise<SessionInfo[]> {
    const response = await fetch(`${this.baseUrl}/api/agent/sessions`);
    if (!response.ok) {
      throw new Error(`List sessions failed: ${response.status}`);
    }
    const data = (await response.json()) as { sessions?: SessionInfo[] };
    return data.sessions ?? [];
  }

  async deleteSession(sessionId: string): Promise<boolean> {
    const response = await fetch(
      `${this.baseUrl}/api/agent/sessions/${sessionId}`,
      { method: 'DELETE' }
    );
    return response.status === 200;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Search
  // ─────────────────────────────────────────────────────────────────────────

  async search(
    query: string,
    searchType: string = 'semantic',
    limit: number = 20
  ): Promise<SearchResponse> {
    const response = await fetch(`${this.baseUrl}/api/query/search`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query,
        type: searchType,
        filters: { limit },
      }),
    });

    if (!response.ok) {
      throw new Error(`Search failed: ${response.status}`);
    }

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

  // ─────────────────────────────────────────────────────────────────────────
  // Indexing
  // ─────────────────────────────────────────────────────────────────────────

  async indexStatus(repoPath: string): Promise<IndexStatus> {
    const url = new URL(`${this.baseUrl}/api/index/status`);
    url.searchParams.set('repo_path', repoPath);

    const response = await fetch(url.toString());
    if (!response.ok) {
      throw new Error(`Index status failed: ${response.status}`);
    }

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

  async *indexStartStream(
    repoPath: string,
    incremental: boolean = false
  ): AsyncGenerator<string, void, unknown> {
    const payload = {
      repo_path: repoPath,
      options: { incremental },
    };

    const response = await fetch(`${this.baseUrl}/api/index/start`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Index start failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Auth
  // ─────────────────────────────────────────────────────────────────────────

  async authLogin(): Promise<AuthLoginResponse> {
    const response = await fetch(`${this.baseUrl}/api/auth/login`, {
      method: 'POST',
    });
    if (!response.ok) {
      throw new Error(`Auth login failed: ${response.status}`);
    }
    return response.json() as Promise<AuthLoginResponse>;
  }

  async authPoll(userCode: string): Promise<AuthPollResponse> {
    const response = await fetch(
      `${this.baseUrl}/api/auth/login/poll/${userCode}`,
      { method: 'POST' }
    );
    if (!response.ok) {
      throw new Error(`Auth poll failed: ${response.status}`);
    }
    return response.json() as Promise<AuthPollResponse>;
  }

  async authLogout(): Promise<{ success: boolean }> {
    const response = await fetch(`${this.baseUrl}/api/auth/logout`, {
      method: 'POST',
    });
    if (!response.ok) {
      throw new Error(`Auth logout failed: ${response.status}`);
    }
    return response.json() as Promise<{ success: boolean }>;
  }

  async authStatus(): Promise<AuthStatusResponse> {
    const response = await fetch(`${this.baseUrl}/api/auth/status`);
    if (!response.ok) {
      throw new Error(`Auth status failed: ${response.status}`);
    }
    return response.json() as Promise<AuthStatusResponse>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Database
  // ─────────────────────────────────────────────────────────────────────────

  async dbInit(): Promise<{ success: boolean }> {
    const response = await fetch(`${this.baseUrl}/api/db/init`, {
      method: 'POST',
    });
    if (!response.ok) {
      throw new Error(`DB init failed: ${response.status}`);
    }
    return response.json() as Promise<{ success: boolean }>;
  }

  async dbClear(confirm: boolean = true): Promise<{ success: boolean }> {
    const url = new URL(`${this.baseUrl}/api/db/clear`);
    url.searchParams.set('confirm', String(confirm));

    const response = await fetch(url.toString(), { method: 'POST' });
    if (!response.ok) {
      throw new Error(`DB clear failed: ${response.status}`);
    }
    return response.json() as Promise<{ success: boolean }>;
  }

  async dbStats(): Promise<DbStats> {
    const response = await fetch(`${this.baseUrl}/api/db/stats`);
    if (!response.ok) {
      throw new Error(`DB stats failed: ${response.status}`);
    }
    return response.json() as Promise<DbStats>;
  }

  async dbTest(): Promise<{ success: boolean }> {
    const response = await fetch(`${this.baseUrl}/api/db/test`);
    if (!response.ok) {
      throw new Error(`DB test failed: ${response.status}`);
    }
    return response.json() as Promise<{ success: boolean }>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Analytics
  // ─────────────────────────────────────────────────────────────────────────

  async analyzePagerank(
    top: number = 20,
    damping: number = 0.85
  ): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/analyze/pagerank`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ top, damping }),
    });
    if (!response.ok) {
      throw new Error(`Analyze pagerank failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  async analyzeCommunities(
    resolution: number = 1.0,
    minSize: number = 3,
    top: number = 20
  ): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/analyze/communities`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ resolution, min_size: minSize, top }),
    });
    if (!response.ok) {
      throw new Error(`Analyze communities failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  async analyzeAreas(
    depth: number = 2,
    days: number = 30,
    top: number = 20,
    sort: string = 'focus',
    files: boolean = false
  ): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/analyze/areas`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ depth, days, top, sort, files }),
    });
    if (!response.ok) {
      throw new Error(`Analyze areas failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Embeddings
  // ─────────────────────────────────────────────────────────────────────────

  async embedStatus(): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/embed/status`);
    if (!response.ok) {
      throw new Error(`Embed status failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  async embedModels(): Promise<string[]> {
    const response = await fetch(`${this.baseUrl}/api/embed/models`);
    if (!response.ok) {
      throw new Error(`Embed models failed: ${response.status}`);
    }
    const data = (await response.json()) as { models?: string[] };
    return data.models ?? [];
  }

  async embedIndex(
    includePrs: boolean = true,
    includeFunctions: boolean = true,
    includeClasses: boolean = true,
    reindex: boolean = false
  ): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/embed/index`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        include_prs: includePrs,
        include_functions: includeFunctions,
        include_classes: includeClasses,
        reindex,
      }),
    });
    if (!response.ok) {
      throw new Error(`Embed index failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Rules/Templates
  // ─────────────────────────────────────────────────────────────────────────

  async rulesList(): Promise<string[]> {
    const response = await fetch(`${this.baseUrl}/api/rules/list`);
    if (!response.ok) {
      throw new Error(`Rules list failed: ${response.status}`);
    }
    const data = (await response.json()) as { templates?: string[] };
    return data.templates ?? [];
  }

  async rulesGet(templateName: string): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/rules/${templateName}`);
    if (!response.ok) {
      throw new Error(`Rules get failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  async rulesInit(
    globalTemplates: boolean = false,
    force: boolean = false
  ): Promise<Record<string, unknown>> {
    const url = new URL(`${this.baseUrl}/api/rules/init`);
    url.searchParams.set('global_templates', String(globalTemplates));
    url.searchParams.set('force', String(force));

    const response = await fetch(url.toString(), { method: 'POST' });
    if (!response.ok) {
      throw new Error(`Rules init failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Project MD
  // ─────────────────────────────────────────────────────────────────────────

  async *projectmdGenerateStream(
    output: string = 'PROJECT.md',
    save: boolean = true,
    model?: string
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = { output, save };
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/projectmd/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Project MD generate failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Spec & Tasks
  // ─────────────────────────────────────────────────────────────────────────

  async *specGenerateStream(
    feature: string,
    model?: string,
    save: boolean = false
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = { feature, save };
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/spec/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Spec generate failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  async *tasksGenerateStream(
    specName?: string,
    model?: string,
    save: boolean = false
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = { save };
    if (specName) payload['spec_name'] = specName;
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/tasks/generate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Tasks generate failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  async planContext(
    description: string,
    similarPrs: number = 5
  ): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/plan/context`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ description, similar_prs: similarPrs }),
    });
    if (!response.ok) {
      throw new Error(`Plan context failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Research
  // ─────────────────────────────────────────────────────────────────────────

  async *researchStream(
    goal: string,
    maxIterations: number = getMaxIterations(),
    budget: number = 50,
    model?: string
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = {
      goal,
      max_iterations: maxIterations,
      budget,
    };
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/research/run`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Research failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Team
  // ─────────────────────────────────────────────────────────────────────────

  async teamFocus(
    days: number = 14,
    model?: string
  ): Promise<Record<string, unknown>> {
    const payload: Record<string, unknown> = { days };
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/team/focus`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
    if (!response.ok) {
      throw new Error(`Team focus failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Swarm
  // ─────────────────────────────────────────────────────────────────────────

  async *swarmRunStream(
    tasks: string[],
    model?: string,
    autoMerge: boolean = false
  ): AsyncGenerator<string, void, unknown> {
    const payload: Record<string, unknown> = { tasks, auto_merge: autoMerge };
    if (model) payload['model'] = model;

    const response = await fetch(`${this.baseUrl}/api/swarm/run`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Swarm run failed: ${response.status}`);
    }

    yield* this.readSSEStream(response);
  }

  async swarmStatus(): Promise<Record<string, unknown>> {
    const response = await fetch(`${this.baseUrl}/api/swarm/status`);
    if (!response.ok) {
      throw new Error(`Swarm status failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Todos
  // ─────────────────────────────────────────────────────────────────────────

  async getTodos(sessionId: string): Promise<TodosResponse> {
    const response = await fetch(
      `${this.baseUrl}/api/agent/chat/${sessionId}/todos`
    );
    if (!response.ok) {
      throw new Error(`Get todos failed: ${response.status}`);
    }
    return response.json() as Promise<TodosResponse>;
  }

  async addTodo(
    sessionId: string,
    title: string,
    description: string = ''
  ): Promise<Record<string, unknown>> {
    const url = new URL(`${this.baseUrl}/api/agent/chat/${sessionId}/todos`);
    url.searchParams.set('title', title);
    url.searchParams.set('description', description);

    const response = await fetch(url.toString(), { method: 'POST' });
    if (!response.ok) {
      throw new Error(`Add todo failed: ${response.status}`);
    }
    return response.json() as Promise<Record<string, unknown>>;
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Generic HTTP methods
  // ─────────────────────────────────────────────────────────────────────────

  async get(path: string): Promise<Response> {
    return fetch(`${this.baseUrl}${path}`);
  }

  async post(path: string, json?: Record<string, unknown>): Promise<Response> {
    return fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      headers: json ? { 'Content-Type': 'application/json' } : undefined,
      body: json ? JSON.stringify(json) : undefined,
    });
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Lifecycle
  // ─────────────────────────────────────────────────────────────────────────

  abort(): void {
    this.abortController?.abort();
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Helpers
  // ─────────────────────────────────────────────────────────────────────────

  private async *readSSEStream(
    response: Response
  ): AsyncGenerator<string, void, unknown> {
    const reader = response.body?.getReader();
    if (!reader) {
      throw new Error('No response body');
    }

    const decoder = new TextDecoder();
    let buffer = '';

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() ?? '';

        for (const line of lines) {
          if (line.trim()) {
            yield line;
          }
        }
      }

      // Process remaining buffer
      if (buffer.trim()) {
        yield buffer;
      }
    } finally {
      reader.releaseLock();
    }
  }
}
