import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { McpServerMapper } from './mcp_server.mapper.js';
import type { IMcpServerConfig, IMcpServerPromptDefinition, IMcpServerRawState, IMcpServerResourceDefinition, IMcpServerSnapshot, IMcpServerToolDefinition } from './mcp_server.types.js';
import { mcpServerDefaultHttpPath, mcpServerDefaultProtocolVersion, mcpServerDefaultTimeoutMs, mcpServerProfile } from './mcp_server.types.js';

export class McpServerApiError extends Error {}
export class McpServerConnectionError extends McpServerApiError {}
export class McpServerProtocolError extends McpServerApiError {}
export class McpServerUnsupportedCommandError extends McpServerApiError {}

interface IMcpServerJsonRpcSuccess<TValue = unknown> {
  jsonrpc: '2.0';
  id?: string | number | null;
  result: TValue;
}

interface IMcpServerJsonRpcFailure {
  jsonrpc: '2.0';
  id?: string | number | null;
  error: {
    code: number;
    message: string;
    data?: unknown;
  };
}

type TMcpServerJsonRpcResponse<TValue = unknown> = IMcpServerJsonRpcSuccess<TValue> | IMcpServerJsonRpcFailure;

export class McpServerClient extends SimpleLocalClient<IMcpServerConfig> {
  private cachedSnapshot?: IMcpServerSnapshot;
  private requestId = 1;

  constructor(private readonly mcpServerConfig: IMcpServerConfig) {
    super(mcpServerProfile, mcpServerConfig);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IMcpServerSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return McpServerMapper.clone(this.cachedSnapshot) as IMcpServerSnapshot;
    }

    if (this.endpointUrl()) {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
        return McpServerMapper.clone(this.cachedSnapshot) as IMcpServerSnapshot;
      } catch (errorArg) {
        this.cachedSnapshot = this.mcpServerOfflineSnapshot(this.errorMessage(errorArg));
        return McpServerMapper.clone(this.cachedSnapshot) as IMcpServerSnapshot;
      }
    }

    return await super.getSnapshot(forceRefreshArg) as IMcpServerSnapshot;
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = Boolean(this.endpointUrl()) && snapshot.online && !snapshot.error && snapshot.source === 'http';
    return { success, error: success ? undefined : snapshot.error || 'MCP Server refresh requires a reachable streamable HTTP endpoint.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<IMcpServerSnapshot> {
    const endpointUrl = this.requireEndpointUrl();
    const initialize = await this.initialize();
    const [tools, prompts, resources] = await Promise.all([
      this.listTools().catch(() => []),
      this.listPrompts().catch(() => []),
      this.listResources().catch(() => []),
    ]);
    const rawData: IMcpServerRawState = {
      protocolVersion: stringValue(initialize.protocolVersion) || this.mcpServerConfig.protocolVersion || mcpServerDefaultProtocolVersion,
      serverInfo: objectValue(initialize.serverInfo),
      capabilities: objectValue(initialize.capabilities),
      tools,
      prompts,
      resources,
      toolCount: tools.length,
      promptCount: prompts.length,
      resourceCount: resources.length,
      initialized: true,
      endpointUrl,
      fetchedAt: new Date().toISOString(),
    };
    return McpServerMapper.toSnapshot({ config: this.mcpServerConfig, rawData, online: true, source: 'http' }) as IMcpServerSnapshot;
  }

  public async initialize(): Promise<Record<string, unknown>> {
    const result = await this.rpc<Record<string, unknown>>('initialize', {
      protocolVersion: this.mcpServerConfig.protocolVersion || mcpServerDefaultProtocolVersion,
      capabilities: {},
      clientInfo: {
        name: 'smarthome.exchange',
        version: '0.1.0',
      },
    });
    await this.notification('notifications/initialized', {}).catch(() => undefined);
    return result;
  }

  public async listTools(): Promise<IMcpServerToolDefinition[]> {
    const result = await this.rpc<Record<string, unknown>>('tools/list', {});
    const tools = result.tools;
    return Array.isArray(tools) ? tools.filter(isMcpServerTool) : [];
  }

  public async listPrompts(): Promise<IMcpServerPromptDefinition[]> {
    const result = await this.rpc<Record<string, unknown>>('prompts/list', {});
    const prompts = result.prompts;
    return Array.isArray(prompts) ? prompts.filter(isMcpServerPrompt) : [];
  }

  public async listResources(): Promise<IMcpServerResourceDefinition[]> {
    const result = await this.rpc<Record<string, unknown>>('resources/list', {});
    const resources = result.resources;
    return Array.isArray(resources) ? resources.filter(isMcpServerResource) : [];
  }

  public async callTool(nameArg: string, argumentsArg: Record<string, unknown> = {}): Promise<unknown> {
    return await this.rpc('tools/call', { name: nameArg, arguments: argumentsArg });
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.mcpServerConfig.commandExecutor || this.mcpServerConfig.client?.execute) {
      return super.execute(requestArg);
    }
    if (requestArg.service !== 'call_tool') {
      return super.execute(requestArg);
    }

    try {
      const data = requestArg.data || {};
      const toolName = stringValue(data.name ?? data.toolName ?? data.tool_name ?? requestArg.target.entityId);
      if (!toolName) {
        throw new McpServerUnsupportedCommandError('MCP Server call_tool requires data.name or data.toolName.');
      }
      const args = objectValue(data.arguments) || objectValue(data.args) || {};
      const result = await this.callTool(toolName, args);
      this.cachedSnapshot = undefined;
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: this.errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await super.destroy();
  }

  private async rpc<TValue>(methodArg: string, paramsArg: Record<string, unknown>): Promise<TValue> {
    const id = this.requestId++;
    const response = await this.postJsonRpc<TValue>({ jsonrpc: '2.0', id, method: methodArg, params: paramsArg });
    if ('error' in response) {
      throw new McpServerProtocolError(`MCP Server ${methodArg} failed: ${response.error.message}`);
    }
    return response.result;
  }

  private async notification(methodArg: string, paramsArg: Record<string, unknown>): Promise<void> {
    await this.postJsonRpc({ jsonrpc: '2.0', method: methodArg, params: paramsArg }, true);
  }

  private async postJsonRpc<TValue>(bodyArg: Record<string, unknown>, notificationArg = false): Promise<TMcpServerJsonRpcResponse<TValue>> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.mcpServerConfig.timeoutMs || mcpServerDefaultTimeoutMs);
    try {
      const response = await globalThis.fetch(this.requireEndpointUrl(), {
        method: 'POST',
        headers: this.headers(),
        body: JSON.stringify(bodyArg),
        signal: controller.signal,
      });
      const text = await response.text();
      if (notificationArg && (response.status === 202 || !text.trim())) {
        return { jsonrpc: '2.0', result: undefined as TValue };
      }
      if (!response.ok) {
        throw new McpServerConnectionError(`MCP Server HTTP request failed with ${response.status}: ${text.slice(0, 240)}`);
      }
      const payload = parseMcpServerResponse(text, response.headers.get('content-type') || '');
      if (!payload) {
        throw new McpServerProtocolError('MCP Server endpoint returned an empty response.');
      }
      return payload as TMcpServerJsonRpcResponse<TValue>;
    } catch (errorArg) {
      if (errorArg instanceof McpServerApiError) {
        throw errorArg;
      }
      throw new McpServerConnectionError(this.errorMessage(errorArg));
    } finally {
      clearTimeout(timeout);
    }
  }

  private headers(): Record<string, string> {
    const headers: Record<string, string> = {
      accept: 'application/json, text/event-stream',
      'content-type': 'application/json',
      'mcp-protocol-version': this.mcpServerConfig.protocolVersion || mcpServerDefaultProtocolVersion,
      ...(this.mcpServerConfig.headers || {}),
    };
    const token = this.mcpServerConfig.accessToken || this.mcpServerConfig.token;
    if (token) {
      headers.authorization = `Bearer ${token}`;
    }
    return headers;
  }

  private endpointUrl(): string | undefined {
    if (this.mcpServerConfig.url) {
      return this.mcpServerConfig.url;
    }
    if (!this.mcpServerConfig.host) {
      return undefined;
    }
    const protocol = this.mcpServerConfig.useTls ? 'https' : 'http';
    const port = this.mcpServerConfig.port ? `:${this.mcpServerConfig.port}` : '';
    const path = this.mcpServerConfig.path || mcpServerDefaultHttpPath;
    return `${protocol}://${this.mcpServerConfig.host}${port}${path.startsWith('/') ? path : `/${path}`}`;
  }

  private requireEndpointUrl(): string {
    const endpointUrl = this.endpointUrl();
    if (!endpointUrl) {
      throw new McpServerConnectionError('MCP Server streamable HTTP access requires config.url or config.host.');
    }
    return endpointUrl;
  }

  private mcpServerOfflineSnapshot(errorArg: string): IMcpServerSnapshot {
    return McpServerMapper.toSnapshot({ config: this.mcpServerConfig, rawData: this.mcpServerConfig.rawData, online: false, source: 'runtime', error: errorArg }) as IMcpServerSnapshot;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}

const parseMcpServerResponse = (textArg: string, contentTypeArg: string): unknown => {
  const text = contentTypeArg.includes('text/event-stream') ? firstSseJson(textArg) : textArg;
  if (!text.trim()) {
    return undefined;
  }
  try {
    const parsed = JSON.parse(text) as unknown;
    if (Array.isArray(parsed)) {
      return parsed[0];
    }
    return parsed;
  } catch (errorArg) {
    throw new McpServerProtocolError(`MCP Server endpoint returned invalid JSON: ${errorArg instanceof Error ? errorArg.message : String(errorArg)}`);
  }
};

const firstSseJson = (textArg: string): string => {
  const chunks = textArg.split(/\r?\n\r?\n/);
  for (const chunk of chunks) {
    const data = chunk.split(/\r?\n/)
      .filter((lineArg) => lineArg.startsWith('data:'))
      .map((lineArg) => lineArg.slice(5).trim())
      .join('\n')
      .trim();
    if (data && data !== '[DONE]') {
      return data;
    }
  }
  return '';
};

const isMcpServerTool = (valueArg: unknown): valueArg is IMcpServerToolDefinition => Boolean(valueArg && typeof valueArg === 'object' && typeof (valueArg as IMcpServerToolDefinition).name === 'string');
const isMcpServerPrompt = (valueArg: unknown): valueArg is IMcpServerPromptDefinition => Boolean(valueArg && typeof valueArg === 'object' && typeof (valueArg as IMcpServerPromptDefinition).name === 'string');
const isMcpServerResource = (valueArg: unknown): valueArg is IMcpServerResourceDefinition => Boolean(valueArg && typeof valueArg === 'object' && typeof (valueArg as IMcpServerResourceDefinition).uri === 'string');

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
