import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { McpMapper } from './mcp.mapper.js';
import type { IMcpConfig, IMcpRawState, IMcpSnapshot, IMcpToolDefinition } from './mcp.types.js';
import { mcpDefaultHttpPath, mcpDefaultProtocolVersion, mcpDefaultTimeoutMs, mcpProfile } from './mcp.types.js';

export class McpApiError extends Error {}
export class McpConnectionError extends McpApiError {}
export class McpProtocolError extends McpApiError {}
export class McpUnsupportedCommandError extends McpApiError {}

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

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

type TMcpJsonRpcResponse<TValue = unknown> = IMcpJsonRpcSuccess<TValue> | IMcpJsonRpcFailure;

export class McpClient extends SimpleLocalClient<IMcpConfig> {
  private cachedSnapshot?: IMcpSnapshot;
  private requestId = 1;

  constructor(private readonly mcpConfig: IMcpConfig) {
    super(mcpProfile, mcpConfig);
  }

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

    if (this.endpointUrl()) {
      try {
        this.cachedSnapshot = await this.fetchSnapshot();
        return McpMapper.clone(this.cachedSnapshot) as IMcpSnapshot;
      } catch (errorArg) {
        this.cachedSnapshot = this.mcpOfflineSnapshot(this.errorMessage(errorArg));
        return McpMapper.clone(this.cachedSnapshot) as IMcpSnapshot;
      }
    }

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

  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 refresh requires a reachable streamable HTTP endpoint.', data: snapshot };
  }

  public async fetchSnapshot(): Promise<IMcpSnapshot> {
    const endpointUrl = this.requireEndpointUrl();
    const initialize = await this.initialize();
    const tools = await this.listTools();
    const rawData: IMcpRawState = {
      protocolVersion: stringValue(initialize.protocolVersion) || this.mcpConfig.protocolVersion || mcpDefaultProtocolVersion,
      serverInfo: objectValue(initialize.serverInfo),
      capabilities: objectValue(initialize.capabilities),
      tools,
      toolCount: tools.length,
      initialized: true,
      endpointUrl,
      fetchedAt: new Date().toISOString(),
    };
    return McpMapper.toSnapshot({ config: this.mcpConfig, rawData, online: true, source: 'http' }) as IMcpSnapshot;
  }

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

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

  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.mcpConfig.commandExecutor || this.mcpConfig.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 McpUnsupportedCommandError('MCP 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 McpProtocolError(`MCP ${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<TMcpJsonRpcResponse<TValue>> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.mcpConfig.timeoutMs || mcpDefaultTimeoutMs);
    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 McpConnectionError(`MCP HTTP request failed with ${response.status}: ${text.slice(0, 240)}`);
      }
      const payload = parseMcpResponse(text, response.headers.get('content-type') || '');
      if (!payload) {
        throw new McpProtocolError('MCP endpoint returned an empty response.');
      }
      return payload as TMcpJsonRpcResponse<TValue>;
    } catch (errorArg) {
      if (errorArg instanceof McpApiError) {
        throw errorArg;
      }
      throw new McpConnectionError(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.mcpConfig.protocolVersion || mcpDefaultProtocolVersion,
      ...(this.mcpConfig.headers || {}),
    };
    const token = this.mcpConfig.accessToken || this.mcpConfig.token;
    if (token) {
      headers.authorization = `Bearer ${token}`;
    }
    return headers;
  }

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

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

  private mcpOfflineSnapshot(errorArg: string): IMcpSnapshot {
    return McpMapper.toSnapshot({ config: this.mcpConfig, rawData: this.mcpConfig.rawData, online: false, source: 'runtime', error: errorArg }) as IMcpSnapshot;
  }

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

const parseMcpResponse = (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 McpProtocolError(`MCP 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 isMcpTool = (valueArg: unknown): valueArg is IMcpToolDefinition => Boolean(valueArg && typeof valueArg === 'object' && typeof (valueArg as IMcpToolDefinition).name === '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;
