import { v, Infer } from 'convex/values';

import {
  jsonValueValidator,
  jsonRecordValidator,
} from '../../../lib/shared/schemas/utils/json-value';
import { internal } from '../../_generated/api';
import { internalAction, type ActionCtx } from '../../_generated/server';
import { isSqlIntegration } from '../../integrations/helpers';
import type { LoadedIntegration } from '../../integrations/load_integration';
import type { SqlOperation } from '../../integrations/types';
import {
  toConvexJsonRecord,
  toConvexJsonValue,
} from '../../lib/type_cast_helpers';
import { resolveOrgSlug } from '../../organizations/resolve_org_slug';
import { decryptSqlCredentials } from '../../workflow_engine/action_defs/integration/helpers/decrypt_sql_credentials';
import {
  requiresApproval,
  getOperationType,
} from '../../workflow_engine/action_defs/integration/helpers/detect_write_operation';
import { getIntrospectColumnsQuery } from '../../workflow_engine/action_defs/integration/helpers/get_introspect_columns_query';
import { getIntrospectTablesQuery } from '../../workflow_engine/action_defs/integration/helpers/get_introspect_tables_query';
import { getIntrospectionOperations } from '../../workflow_engine/action_defs/integration/helpers/get_introspection_operations';
import { isIntrospectionOperation } from '../../workflow_engine/action_defs/integration/helpers/is_introspection_operation';
import { validateRequiredParameters } from '../../workflow_engine/action_defs/integration/helpers/validate_required_parameters';
import { integrationAction } from '../../workflow_engine/action_defs/integration/integration_action';
import { sanitizeDepth } from '../../workflow_engine/helpers/serialization/sanitize_depth';
import type { IntegrationOperationMetadataLocal } from './types';

type ConvexJsonValue = Infer<typeof jsonValueValidator>;

const INTEGRATION_MAX_DEPTH = 8;

// =============================================================================
// EXECUTE INTEGRATION
// =============================================================================

export const executeIntegration = internalAction({
  args: {
    organizationId: v.string(),
    integrationName: v.string(),
    operation: v.string(),
    params: v.optional(jsonRecordValidator),
    skipApprovalCheck: v.optional(v.boolean()),
    threadId: v.optional(v.string()),
    messageId: v.optional(v.string()),
  },
  handler: async (ctx, args): Promise<ConvexJsonValue> => {
    const {
      organizationId,
      integrationName,
      operation,
      params,
      skipApprovalCheck,
      threadId,
      messageId,
    } = args;

    console.log('[executeIntegration] Received context:', {
      hasThreadId: threadId !== undefined,
      hasMessageId: messageId !== undefined,
      threadId: threadId,
      messageId: messageId,
      operation,
      integrationName,
      paramsKeys: params ? Object.keys(params) : 'undefined',
    });

    const result = await integrationAction.execute(
      ctx,
      {
        name: integrationName,
        operation,
        params: params || {},
        skipApprovalCheck: skipApprovalCheck || false,
        threadId,
        messageId,
      },
      {
        organizationId,
      },
    );

    return toConvexJsonValue(sanitizeDepth(result, 0, INTEGRATION_MAX_DEPTH));
  },
});

// =============================================================================
// EXECUTE APPROVED OPERATION
// =============================================================================

async function notifyThread(
  ctx: ActionCtx,
  approval: { threadId?: string; organizationId: string },
  messageContent: string,
): Promise<void> {
  if (!approval.threadId) return;

  const threadMeta = await ctx.runQuery(
    internal.threads.internal_queries.getThreadMetadata,
    { threadId: approval.threadId },
  );

  if (threadMeta?.agentSlug) {
    await ctx.scheduler.runAfter(
      0,
      internal.agent_tools.integrations.trigger_completion_action
        .triggerCompletionWithAgent,
      {
        threadId: approval.threadId,
        organizationId: approval.organizationId,
        agentSlug: threadMeta.agentSlug,
        messageContent,
      },
    );
  } else {
    await ctx.runMutation(
      internal.agent_tools.integrations.internal_mutations.saveSystemMessage,
      { threadId: approval.threadId, content: messageContent },
    );
  }
}

function buildCompletionMessage(
  metadata: IntegrationOperationMetadataLocal,
  result: unknown,
): string {
  const resultSummary = JSON.stringify(result, null, 2);
  // Truncate large results to avoid bloating the system message
  const truncated =
    resultSummary.length > 2000
      ? resultSummary.slice(0, 2000) + '\n... (truncated)'
      : resultSummary;

  return (
    `[INTEGRATION_OPERATION_COMPLETED]\n` +
    `Integration "${metadata.integrationName}" operation "${metadata.operationName}" completed successfully.\n\n` +
    `Execution result:\n${truncated}\n\n` +
    `Instructions:\n` +
    `- Present the operation result to the user in markdown format\n` +
    `- If the result contains data, summarize it clearly\n` +
    `- If the result contains fileReferences with images, you MUST display each image inline using markdown image syntax: ![description](url)\n` +
    `- Never omit images from the response — the user expects to see them directly`
  );
}

function buildErrorMessage(
  metadata: IntegrationOperationMetadataLocal,
  errorMessage: string,
): string {
  return (
    `[INTEGRATION_OPERATION_FAILED]\n` +
    `Integration "${metadata.integrationName}" operation "${metadata.operationName}" failed.\n\n` +
    `Error: ${errorMessage}\n\n` +
    `Instructions:\n` +
    `- Inform the user that the operation failed and explain the error\n` +
    `- Suggest possible fixes or alternative approaches`
  );
}

export const executeApprovedOperation = internalAction({
  args: {
    approvalId: v.id('approvals'),
    approvedBy: v.string(),
  },
  returns: jsonValueValidator,
  handler: async (ctx, args): Promise<ConvexJsonValue> => {
    const approval: {
      _id: unknown;
      status: string;
      resourceType: string;
      organizationId: string;
      threadId?: string;
      metadata?: Record<string, ConvexJsonValue>;
    } | null = await ctx.runQuery(
      internal.approvals.internal_queries.getApprovalById,
      {
        approvalId: args.approvalId,
      },
    );

    if (!approval) {
      throw new Error('Approval not found');
    }

    if (approval.status !== 'executing') {
      throw new Error(
        `Cannot execute operation: approval status is "${approval.status}", expected "executing"`,
      );
    }

    if (approval.resourceType !== 'integration_operation') {
      throw new Error(
        `Invalid approval type: expected "integration_operation", got "${approval.resourceType}"`,
      );
    }

    // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- approval.metadata stored via v.any(); shape is IntegrationOperationMetadataLocal written by our approval creation code
    const metadata = approval.metadata as
      | IntegrationOperationMetadataLocal
      | undefined;

    if (!metadata?.integrationName || !metadata?.operationName) {
      throw new Error(
        'Invalid approval metadata: missing integration or operation name',
      );
    }

    try {
      const result = await ctx.runAction(
        internal.agent_tools.integrations.internal_actions.executeIntegration,
        {
          organizationId: approval.organizationId,
          integrationName: metadata.integrationName,
          operation: metadata.operationName,
          params: toConvexJsonRecord(metadata.parameters),
          skipApprovalCheck: true,
        },
      );

      await ctx.runMutation(
        internal.agent_tools.integrations.internal_mutations
          .updateApprovalWithResult,
        {
          approvalId: args.approvalId,
          executionResult: toConvexJsonValue(result),
          executionError: null,
        },
      );

      // Post completion message to chat and trigger agent response
      await notifyThread(
        ctx,
        approval,
        buildCompletionMessage(metadata, result),
      );

      return toConvexJsonValue(result);
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);

      await ctx.runMutation(
        internal.agent_tools.integrations.internal_mutations
          .updateApprovalWithResult,
        {
          approvalId: args.approvalId,
          executionResult: null,
          executionError: errorMessage,
        },
      );

      // Post error message to chat so the agent can inform the user
      await notifyThread(
        ctx,
        approval,
        buildErrorMessage(metadata, errorMessage),
      );

      throw error;
    }
  },
});

// =============================================================================
// EXECUTE BATCH INTEGRATION
// =============================================================================

const operationResultValidator = v.object({
  id: v.optional(v.string()),
  operation: v.string(),
  success: v.boolean(),
  data: v.optional(jsonValueValidator),
  error: v.optional(v.string()),
  duration: v.optional(v.number()),
  rowCount: v.optional(v.number()),
  requiresApproval: v.optional(v.boolean()),
  approvalId: v.optional(v.string()),
});

const batchResultValidator = v.object({
  success: v.boolean(),
  integration: v.string(),
  results: v.array(operationResultValidator),
  stats: v.object({
    totalTime: v.number(),
    successCount: v.number(),
    failureCount: v.number(),
    approvalCount: v.number(),
  }),
});

interface OperationResult {
  id?: string;
  operation: string;
  success: boolean;
  data?: ConvexJsonValue;
  error?: string;
  duration?: number;
  rowCount?: number;
  requiresApproval?: boolean;
  approvalId?: string;
}

type BatchResult = Infer<typeof batchResultValidator>;

export const executeBatchIntegration = internalAction({
  args: {
    organizationId: v.string(),
    integrationName: v.string(),
    operations: v.array(
      v.object({
        id: v.optional(v.string()),
        operation: v.string(),
        params: v.optional(jsonRecordValidator),
      }),
    ),
    threadId: v.optional(v.string()),
    messageId: v.optional(v.string()),
  },
  returns: batchResultValidator,
  handler: async (ctx, args): Promise<BatchResult> => {
    const { organizationId, integrationName, operations, threadId, messageId } =
      args;
    const startTime = Date.now();

    console.log('[executeBatchIntegration] Starting batch:', {
      integrationName,
      operationCount: operations.length,
      operations: operations.map((op) => op.operation),
    });

    // 1. Load integration config ONCE
    const orgSlug = await resolveOrgSlug(ctx, organizationId);
    const integration = await ctx.runAction(
      internal.integrations.load_integration.loadIntegration,
      { orgSlug, organizationId, slug: integrationName },
    );

    if (!integration) {
      return {
        success: false,
        integration: integrationName,
        results: operations.map((op) => ({
          id: op.id,
          operation: op.operation,
          success: false,
          error: `Integration not found: "${integrationName}"`,
        })),
        stats: {
          totalTime: Date.now() - startTime,
          successCount: 0,
          failureCount: operations.length,
          approvalCount: 0,
        },
      };
    }

    const integrationType = integration.type ?? 'rest_api';

    // For SQL integrations, optimize by decrypting credentials once
    if (integrationType === 'sql' && isSqlIntegration(integration)) {
      return executeSqlBatch(
        ctx,
        integration,
        operations,
        threadId,
        messageId,
        startTime,
      );
    }

    // For REST API integrations, fall back to sequential execution
    return executeRestApiBatch(
      ctx,
      integration,
      operations,
      organizationId,
      threadId,
      messageId,
      startTime,
    );
  },
});

async function executeSqlBatch(
  ctx: ActionCtx,
  integration: LoadedIntegration & {
    sqlConnectionConfig: NonNullable<LoadedIntegration['sqlConnectionConfig']>;
    sqlOperations: NonNullable<LoadedIntegration['sqlOperations']>;
  },
  operations: Array<{
    id?: string;
    operation: string;
    params?: Record<string, unknown>;
  }>,
  threadId: string | undefined,
  messageId: string | undefined,
  startTime: number,
) {
  const { sqlConnectionConfig, sqlOperations } = integration;

  const sqlServer = sqlConnectionConfig?.server;
  const sqlDatabase = sqlConnectionConfig?.database;

  if (!sqlServer || !sqlDatabase) {
    return {
      success: false,
      integration: integration.name,
      results: operations.map((op) => ({
        id: op.id,
        operation: op.operation,
        success: false,
        error:
          'SQL integration is missing server or database configuration. Configure it in the integration settings.',
      })),
      stats: {
        approvalCount: 0,
        successCount: 0,
        totalTime: Date.now() - startTime,
        failureCount: operations.length,
      },
    };
  }

  let credentials: { username: string; password: string };
  try {
    credentials = await decryptSqlCredentials(ctx, integration);
  } catch (error) {
    return {
      success: false,
      integration: integration.name,
      results: operations.map((op) => ({
        id: op.id,
        operation: op.operation,
        success: false,
        error: `Failed to decrypt credentials: ${error instanceof Error ? error.message : String(error)}`,
      })),
      stats: {
        totalTime: Date.now() - startTime,
        successCount: 0,
        failureCount: operations.length,
        approvalCount: 0,
      },
    };
  }

  const results = await Promise.allSettled(
    operations.map(async (op) => {
      const opStartTime = Date.now();
      const params = op.params || {};

      try {
        let query: string;
        let queryParams: Record<string, unknown> = params;
        let operationConfig: SqlOperation | undefined;

        if (isIntrospectionOperation(op.operation)) {
          if (op.operation === 'introspect_tables') {
            query = getIntrospectTablesQuery(sqlConnectionConfig.engine);
            queryParams = {};
          } else if (op.operation === 'introspect_columns') {
            if (
              typeof params.schemaName !== 'string' ||
              typeof params.tableName !== 'string'
            ) {
              throw new Error(
                'introspect_columns requires schemaName and tableName string parameters',
              );
            }
            const introspectionQuery = getIntrospectColumnsQuery(
              sqlConnectionConfig.engine,
              params.schemaName,
              params.tableName,
            );
            query = introspectionQuery.query;
            queryParams = introspectionQuery.params;
          } else {
            throw new Error(`Unknown introspection operation: ${op.operation}`);
          }
        } else {
          operationConfig = sqlOperations.find(
            (sqlOp) => sqlOp.name === op.operation,
          );

          if (!operationConfig) {
            const availableOps = [
              ...getIntrospectionOperations(),
              ...sqlOperations.map((sqlOp) => sqlOp.name),
            ].join(', ');
            throw new Error(
              `Operation "${op.operation}" not found. Available: ${availableOps}`,
            );
          }

          query = operationConfig.query;
          validateRequiredParameters(operationConfig, params, op.operation);

          if (requiresApproval(operationConfig)) {
            const operationType = getOperationType(operationConfig);

            const approvalId = await ctx.runMutation(
              internal.agent_tools.integrations.internal_mutations
                .createIntegrationApproval,
              {
                organizationId: integration.organizationId,
                integrationId: integration._id,
                integrationName: integration.name,
                integrationType: 'sql',
                operationName: op.operation,
                operationTitle: operationConfig.title || op.operation,
                operationType,
                parameters: toConvexJsonRecord(params),
                threadId,
                messageId,
                estimatedImpact: `This ${operationType} operation will modify data in ${sqlDatabase}`,
              },
            );

            return {
              id: op.id,
              operation: op.operation,
              success: true,
              requiresApproval: true,
              approvalId,
              duration: Date.now() - opStartTime,
            };
          }
        }

        const isWriteOperation = operationConfig
          ? getOperationType(operationConfig) === 'write'
          : false;

        const result = await ctx.runAction(
          internal.node_only.sql.internal_actions.executeQuery,
          {
            engine: sqlConnectionConfig.engine,
            credentials: {
              server: sqlServer,
              port: sqlConnectionConfig.port,
              database: sqlDatabase,
              user: credentials.username,
              password: credentials.password,
              options: sqlConnectionConfig.options,
            },
            query,
            params: toConvexJsonRecord(queryParams),
            security: {
              maxResultRows: sqlConnectionConfig.security?.maxResultRows,
              queryTimeoutMs: sqlConnectionConfig.security?.queryTimeoutMs,
            },
            allowWrite: isWriteOperation,
          },
        );

        if (!result.success) {
          throw new Error(`SQL query failed: ${result.error}`);
        }

        return {
          id: op.id,
          operation: op.operation,
          success: true,
          data: toConvexJsonValue(
            sanitizeDepth(
              {
                name: integration.name,
                operation: op.operation,
                engine: sqlConnectionConfig.engine,
                data: result.data,
                rowCount: result.rowCount,
                duration: result.duration,
              },
              0,
              INTEGRATION_MAX_DEPTH,
            ),
          ),
          duration: Date.now() - opStartTime,
          rowCount: result.rowCount,
        };
      } catch (error) {
        return {
          id: op.id,
          operation: op.operation,
          success: false,
          error: error instanceof Error ? error.message : String(error),
          duration: Date.now() - opStartTime,
        };
      }
    }),
  );

  const processedResults = results.map((result, index): OperationResult => {
    if (result.status === 'fulfilled') {
      return result.value;
    }
    return {
      id: operations[index].id,
      operation: operations[index].operation,
      success: false,
      error: result.reason?.message || 'Unknown error',
    };
  });

  const totalTime = Date.now() - startTime;
  const successCount = processedResults.filter(
    (r) => r.success && !r.requiresApproval,
  ).length;
  const failureCount = processedResults.filter((r) => !r.success).length;
  const approvalCount = processedResults.filter(
    (r) => r.requiresApproval,
  ).length;

  console.log('[executeBatchIntegration] SQL batch complete:', {
    totalTime,
    successCount,
    failureCount,
    approvalCount,
  });

  return {
    success: failureCount === 0,
    integration: integration.name,
    results: processedResults,
    stats: {
      totalTime,
      successCount,
      failureCount,
      approvalCount,
    },
  };
}

async function executeRestApiBatch(
  ctx: ActionCtx,
  integration: LoadedIntegration,
  operations: Array<{
    id?: string;
    operation: string;
    params?: Record<string, unknown>;
  }>,
  organizationId: string,
  threadId: string | undefined,
  messageId: string | undefined,
  startTime: number,
) {
  const results = await Promise.allSettled(
    operations.map(async (op) => {
      const opStartTime = Date.now();

      try {
        const result = await integrationAction.execute(
          ctx,
          {
            name: integration.name,
            operation: op.operation,
            params: op.params || {},
            skipApprovalCheck: false,
            threadId,
            messageId,
          },
          { organizationId },
        );

        const duration = Date.now() - opStartTime;

        if (
          result &&
          typeof result === 'object' &&
          'requiresApproval' in result &&
          result.requiresApproval
        ) {
          const approvalId =
            'approvalId' in result ? String(result.approvalId) : undefined;
          return {
            id: op.id,
            operation: op.operation,
            success: true,
            requiresApproval: true,
            approvalId,
            duration,
          };
        }

        const rowCount =
          result && typeof result === 'object' && 'rowCount' in result
            ? typeof result.rowCount === 'number'
              ? result.rowCount
              : undefined
            : undefined;

        return {
          id: op.id,
          operation: op.operation,
          success: true,
          data: toConvexJsonValue(
            sanitizeDepth(result, 0, INTEGRATION_MAX_DEPTH),
          ),
          duration,
          rowCount,
        };
      } catch (error) {
        return {
          id: op.id,
          operation: op.operation,
          success: false,
          error: error instanceof Error ? error.message : String(error),
          duration: Date.now() - opStartTime,
        } as OperationResult;
      }
    }),
  );

  const processedResults = results.map((result, index): OperationResult => {
    if (result.status === 'fulfilled') {
      return result.value;
    }
    return {
      id: operations[index].id,
      operation: operations[index].operation,
      success: false,
      error: result.reason?.message || 'Unknown error',
    };
  });

  const totalTime = Date.now() - startTime;
  const successCount = processedResults.filter(
    (r) => r.success && !r.requiresApproval,
  ).length;
  const failureCount = processedResults.filter((r) => !r.success).length;
  const approvalCount = processedResults.filter(
    (r) => r.requiresApproval,
  ).length;

  console.log('[executeBatchIntegration] REST API batch complete:', {
    totalTime,
    successCount,
    failureCount,
    approvalCount,
  });

  return {
    success: failureCount === 0,
    integration: integration.name,
    results: processedResults,
    stats: {
      totalTime,
      successCount,
      failureCount,
      approvalCount,
    },
  };
}
