import { getString, isRecord } from '../../lib/utils/type-guards';
import { components } from '../_generated/api';
import type { Id } from '../_generated/dataModel';
import type { QueryCtx, MutationCtx } from '../_generated/server';
import type {
  ApprovalItem,
  ApprovalStatus,
  ApprovalResourceType,
  CreateApprovalArgs,
  UpdateApprovalStatusArgs,
  GetApprovalHistoryArgs,
  ListPendingApprovalsArgs,
  ListApprovalsByOrganizationArgs,
  RemoveRecommendedProductArgs,
  LinkApprovalsToMessageArgs,
} from './types';

export async function getApproval(
  ctx: QueryCtx,
  approvalId: Id<'approvals'>,
): Promise<ApprovalItem | null> {
  return await ctx.db.get(approvalId);
}

export async function getApprovalHistory(
  ctx: QueryCtx,
  args: GetApprovalHistoryArgs,
): Promise<Array<ApprovalItem>> {
  const approvals: Array<ApprovalItem> = [];
  for await (const approval of ctx.db
    .query('approvals')
    .withIndex('by_resource', (q) =>
      q.eq('resourceType', args.resourceType).eq('resourceId', args.resourceId),
    )
    .order('desc')) {
    approvals.push(approval);
  }
  return approvals;
}

export async function getPendingApprovalForResource(
  ctx: QueryCtx,
  args: GetApprovalHistoryArgs,
): Promise<ApprovalItem | null> {
  const approvals = await ctx.db
    .query('approvals')
    .withIndex('by_resourceType_and_resourceId_and_status', (q) =>
      q
        .eq('resourceType', args.resourceType)
        .eq('resourceId', args.resourceId)
        .eq('status', 'pending'),
    )
    .order('desc')
    .take(1);

  return approvals[0] || null;
}

export async function listPendingApprovals(
  ctx: QueryCtx,
  args: ListPendingApprovalsArgs,
): Promise<Array<ApprovalItem>> {
  const limit = args.limit ?? 1000;

  if (args.resourceType) {
    const resourceType = args.resourceType;
    return await ctx.db
      .query('approvals')
      .withIndex('by_org_status_resourceType', (q) =>
        q
          .eq('organizationId', args.organizationId)
          .eq('status', 'pending')
          .eq('resourceType', resourceType),
      )
      .order('desc')
      .take(limit);
  }

  return await ctx.db
    .query('approvals')
    .withIndex('by_org_status', (q) =>
      q.eq('organizationId', args.organizationId).eq('status', 'pending'),
    )
    .order('desc')
    .take(limit);
}

export async function listPendingApprovalsForExecution(
  ctx: QueryCtx,
  executionId: Id<'wfExecutions'>,
): Promise<Array<ApprovalItem>> {
  const pendingApprovals: Array<ApprovalItem> = [];

  for await (const approval of ctx.db
    .query('approvals')
    .withIndex('by_execution', (q) => q.eq('wfExecutionId', executionId))
    .order('desc')) {
    if (approval.status === 'pending') {
      pendingApprovals.push(approval);
    }
  }

  return pendingApprovals;
}

export async function listApprovalsForExecution(
  ctx: QueryCtx,
  executionId: Id<'wfExecutions'>,
): Promise<Array<ApprovalItem>> {
  const approvals: Array<ApprovalItem> = [];
  for await (const approval of ctx.db
    .query('approvals')
    .withIndex('by_execution', (q) => q.eq('wfExecutionId', executionId))
    .order('desc')) {
    approvals.push(approval);
  }
  return approvals;
}

function matchesSearch(approval: ApprovalItem, searchLower: string): boolean {
  const metadata: Record<string, unknown> = approval.metadata ?? {};

  if (
    typeof metadata['customerName'] === 'string' &&
    metadata['customerName'].toLowerCase().includes(searchLower)
  ) {
    return true;
  }

  if (
    typeof metadata['customerEmail'] === 'string' &&
    metadata['customerEmail'].toLowerCase().includes(searchLower)
  ) {
    return true;
  }

  if (Array.isArray(metadata['recommendedProducts'])) {
    const products: unknown[] = metadata['recommendedProducts'];
    if (
      products.some((p) => {
        if (!isRecord(p)) return false;
        const name = getString(p, 'productName') ?? '';
        return name.toLowerCase().includes(searchLower);
      })
    ) {
      return true;
    }
  }

  return false;
}

export async function listApprovalsByOrganization(
  ctx: QueryCtx,
  args: ListApprovalsByOrganizationArgs,
): Promise<Array<ApprovalItem>> {
  const limit = args.limit ?? 1000;
  const searchLower = args.search?.trim().toLowerCase();

  const resourceTypes: string[] =
    typeof args.resourceType === 'string'
      ? [args.resourceType]
      : Array.isArray(args.resourceType)
        ? args.resourceType
        : [];
  const resourceTypeSet =
    resourceTypes.length > 0 ? new Set(resourceTypes) : undefined;

  const result: Array<ApprovalItem> = [];

  // Use the 3-field index when a single resourceType + known status is provided
  const singleResourceType =
    resourceTypes.length === 1 ? resourceTypes[0] : undefined;

  const buildQuery = (status: ApprovalStatus) => {
    if (singleResourceType) {
      return ctx.db
        .query('approvals')
        .withIndex('by_org_status_resourceType', (q) =>
          q
            .eq('organizationId', args.organizationId)
            .eq('status', status)
            // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Convex document field
            .eq('resourceType', singleResourceType as ApprovalResourceType),
        )
        .order('desc');
    }
    return ctx.db
      .query('approvals')
      .withIndex('by_org_status', (q) =>
        q.eq('organizationId', args.organizationId).eq('status', status),
      )
      .order('desc');
  };

  // When using the 3-field index, resourceType is already filtered by the index
  const needsResourceTypeFilter = resourceTypeSet && !singleResourceType;

  const statuses: ApprovalStatus[] = args.status
    ? [args.status]
    : ['pending', 'executing', 'completed', 'rejected'];

  for (const status of statuses) {
    const query = buildQuery(status);

    for await (const approval of query) {
      if (
        needsResourceTypeFilter &&
        !resourceTypeSet.has(approval.resourceType)
      ) {
        continue;
      }

      if (searchLower && !matchesSearch(approval, searchLower)) {
        continue;
      }

      result.push(approval);
      if (result.length >= limit) {
        result.sort((a, b) => b._creationTime - a._creationTime);
        return result;
      }
    }
  }

  result.sort((a, b) => b._creationTime - a._creationTime);
  return result;
}

export async function listActiveApprovalsByOrganization(
  ctx: QueryCtx,
  args: { organizationId: string; limit?: number },
): Promise<Array<ApprovalItem>> {
  const limit = args.limit ?? 1000;
  const result: Array<ApprovalItem> = [];

  for (const status of ['pending', 'executing'] as const) {
    for await (const approval of ctx.db
      .query('approvals')
      .withIndex('by_org_status', (q) =>
        q.eq('organizationId', args.organizationId).eq('status', status),
      )
      .order('desc')) {
      result.push(approval);
      if (result.length >= limit) break;
    }
    if (result.length >= limit) break;
  }

  result.sort((a, b) => b._creationTime - a._creationTime);
  return result;
}

export async function createApproval(
  ctx: MutationCtx,
  args: CreateApprovalArgs,
): Promise<Id<'approvals'>> {
  // Upsert: if a pending approval exists for the same thread + resourceType + resourceId,
  // update its metadata instead of creating a duplicate.
  if (args.threadId) {
    for await (const existing of ctx.db
      .query('approvals')
      .withIndex('by_threadId_status_resourceType', (q) =>
        q
          .eq('threadId', args.threadId)
          .eq('status', 'pending')
          .eq('resourceType', args.resourceType),
      )) {
      if (existing.resourceId === args.resourceId) {
        // Update the existing pending approval with new metadata
        const updatedMetadata = {
          ...(isRecord(existing.metadata) ? existing.metadata : {}),
          updatedAt: Date.now(),
          ...(args.description ? { description: args.description } : {}),
          ...(isRecord(args.metadata) ? args.metadata : {}),
        };
        await ctx.db.patch(existing._id, {
          metadata: updatedMetadata,
          messageId: args.messageId ?? existing.messageId,
        });
        return existing._id;
      }

      // A different pending approval exists on this thread — block creation
      if (existing.status === 'pending' || existing.status === 'executing') {
        throw new Error(
          'Cannot create approval: another approval is already pending on this thread. Wait for the existing approval to be resolved before creating a new one.',
        );
      }
    }
  }

  const approvalId = await ctx.db.insert('approvals', {
    organizationId: args.organizationId,
    wfExecutionId: args.wfExecutionId,
    stepSlug: args.stepSlug,
    status: 'pending',
    resourceType: args.resourceType,
    resourceId: args.resourceId,
    priority: args.priority,
    dueDate: args.dueDate,
    threadId: args.threadId,
    messageId: args.messageId,
    metadata: {
      createdAt: Date.now(),
      ...(args.requestedBy ? { requestedBy: args.requestedBy } : {}),
      ...(args.description ? { description: args.description } : {}),
      ...(isRecord(args.metadata) ? args.metadata : {}),
    },
  });

  return approvalId;
}

export async function updateApprovalStatus(
  ctx: MutationCtx,
  args: UpdateApprovalStatusArgs,
): Promise<void> {
  const current = await ctx.db.get(args.approvalId);
  if (!current) {
    throw new Error('Approval not found');
  }

  let approverName: string | undefined;
  const userRes = await ctx.runQuery(components.betterAuth.adapter.findMany, {
    model: 'user',
    paginationOpts: { cursor: null, numItems: 1 },
    where: [{ field: '_id', value: args.approvedBy, operator: 'eq' }],
  });
  const user = userRes?.page?.[0];
  if (user) {
    approverName =
      (typeof user.name === 'string' ? user.name : undefined) ||
      (typeof user.email === 'string' ? user.email : undefined);
  }

  const currentMetadata: Record<string, unknown> = current.metadata ?? {};
  await ctx.db.patch(args.approvalId, {
    status: args.status,
    approvedBy: args.approvedBy,
    reviewedAt: Date.now(),
    metadata: {
      ...currentMetadata,
      ...(args.comments ? { comments: args.comments } : {}),
      ...(approverName ? { approverName } : {}),
    },
  });
}

export async function removeRecommendedProduct(
  ctx: MutationCtx,
  args: RemoveRecommendedProductArgs,
): Promise<void> {
  const approval = await ctx.db.get(args.approvalId);
  if (!approval) {
    throw new Error('Approval not found');
  }

  if (approval.status !== 'pending') {
    throw new Error('Cannot modify products in non-pending approvals');
  }

  const metadata = approval.metadata || {};
  const recommendedProducts = Array.isArray(metadata.recommendedProducts)
    ? metadata.recommendedProducts
    : [];

  const updatedProducts = recommendedProducts.filter((product: unknown) => {
    if (
      typeof product !== 'object' ||
      product === null ||
      Array.isArray(product)
    ) {
      return true;
    }
    if (!('productId' in product)) return true;
    return product.productId !== args.productId;
  });

  await ctx.db.patch(args.approvalId, {
    metadata: {
      ...metadata,
      recommendedProducts: updatedProducts,
    },
  });
}

export async function linkApprovalsToMessage(
  ctx: MutationCtx,
  args: LinkApprovalsToMessageArgs,
): Promise<number> {
  const { threadId, messageId } = args;

  const resourceTypesToLink = [
    'integration_operation',
    'workflow_creation',
    'workflow_run',
    'workflow_update',
    'human_input_request',
    'document_write',
    'knowledge_write',
    'location_request',
    'mcp_tool_call',
  ] as const;

  const approvalIds: Array<Id<'approvals'>> = [];

  for (const resourceType of resourceTypesToLink) {
    const query = ctx.db
      .query('approvals')
      .withIndex('by_threadId_status_resourceType', (q) =>
        q
          .eq('threadId', threadId)
          .eq('status', 'pending')
          .eq('resourceType', resourceType),
      )
      .filter((q) => q.eq(q.field('messageId'), undefined));

    for await (const approval of query) {
      approvalIds.push(approval._id);
    }
  }

  await Promise.all(approvalIds.map((id) => ctx.db.patch(id, { messageId })));

  return approvalIds.length;
}
