import type { sheets_v4 } from '@googleapis/sheets'
import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
import { ToolingError } from '../../../../types'
import type { ExecutionContext } from '../../../../../base/types'
import { getOAuthCredentials } from '../../../../../../../integrations/oauth/server/credentials'

/**
 * Google Sheets credentials format
 * Stored in credentials table, encrypted
 */
interface GoogleSheetsCredentials {
  accessToken: string
  refreshToken?: string
  expiresAt?: string
}

/**
 * Sheet info returned from metadata calls
 */
interface SheetInfo {
  sheetId: number
  title: string
  index: number
}

/**
 * Read sheet parameters
 */
interface ReadSheetParams {
  spreadsheetId: string
  range: string
}

/**
 * Read sheet result
 */
interface ReadSheetResult {
  values: string[][]
}

/**
 * Write sheet parameters
 */
interface WriteSheetParams {
  spreadsheetId: string
  range: string
  values: string[][]
}

/**
 * Write sheet result
 */
interface WriteSheetResult {
  updatedCells: number
}

/**
 * Append rows parameters
 */
interface AppendRowsParams {
  spreadsheetId: string
  range: string
  values: string[][]
}

/**
 * Append rows result
 */
interface AppendRowsResult {
  updatedRows: number
}

/**
 * Clear range parameters
 */
interface ClearRangeParams {
  spreadsheetId: string
  range: string
}

/**
 * Clear range result
 */
interface ClearRangeResult {
  clearedRange: string
}

/**
 * Get spreadsheet metadata parameters
 */
interface GetSpreadsheetMetadataParams {
  spreadsheetId: string
}

/**
 * Get spreadsheet metadata result
 */
interface GetSpreadsheetMetadataResult {
  title: string
  sheets: SheetInfo[]
}

/**
 * Batch update parameters
 */
interface BatchUpdateParams {
  spreadsheetId: string
  data: Array<{ range: string; values: string[][] }>
}

/**
 * Batch update result
 */
interface BatchUpdateResult {
  totalUpdatedCells: number
  totalUpdatedRows: number
}

// ============================================
// Workflow-friendly method interfaces
// ============================================

/**
 * Get headers parameters
 */
interface GetHeadersParams {
  spreadsheetId: string
  sheetName: string
}

/**
 * Get headers result
 */
interface GetHeadersResult {
  headers: string[]
  columnCount: number
  columnMap: Record<string, string> // header name -> column letter (e.g., { Name: 'A', Id: 'B' })
}

/**
 * Get last row parameters
 */
interface GetLastRowParams {
  spreadsheetId: string
  sheetName: string
  column?: string // Optional column to check (default: A)
}

/**
 * Get last row result
 */
interface GetLastRowResult {
  lastRow: number
  isEmpty: boolean
}

/**
 * Get row by value parameters
 */
interface GetRowByValueParams {
  spreadsheetId: string
  sheetName: string
  searchColumn: string // Column letter (e.g., 'A', 'B')
  searchValue: string
}

/**
 * Get row by value result
 */
interface GetRowByValueResult {
  found: boolean
  rowNumber: number | null
  rowData: string[] | null
  headers: string[]
}

/**
 * Update row by value parameters
 */
interface UpdateRowByValueParams {
  spreadsheetId: string
  sheetName: string
  searchColumn: string
  searchValue: string
  updates: Record<string, string> // Column header -> new value
}

/**
 * Update row by value result
 */
interface UpdateRowByValueResult {
  found: boolean
  rowNumber: number | null
  updatedCells: number
}

/**
 * Upsert row parameters
 */
interface UpsertRowParams {
  spreadsheetId: string
  sheetName: string
  keyColumn: string // Column to match on (e.g., 'A' or header name)
  keyValue: string
  rowData: Record<string, string> // Column header -> value
}

/**
 * Upsert row result
 */
interface UpsertRowResult {
  action: 'inserted' | 'updated'
  rowNumber: number
  updatedCells: number
}

/**
 * Filter rows parameters
 */
interface FilterRowsParams {
  spreadsheetId: string
  sheetName: string
  filterColumn: string
  filterValue: string
  operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith'
}

/**
 * Filter rows result
 */
interface FilterRowsResult {
  rows: Array<{
    rowNumber: number
    data: string[]
  }>
  matchCount: number
  headers: string[]
}

/**
 * Delete row by value parameters
 */
interface DeleteRowByValueParams {
  spreadsheetId: string
  sheetName: string
  searchColumn: string
  searchValue: string
}

/**
 * Delete row by value result
 */
interface DeleteRowByValueResult {
  found: boolean
  deletedRowNumber: number | null
}

/**
 * Google Sheets adapter for spreadsheet operations via Google Sheets API v4
 *
 * Core methods:
 * - readSheet: Read cell range from spreadsheet
 * - writeSheet: Write to cell range (overwrites existing data)
 * - appendRows: Append rows to end of table
 * - clearRange: Clear values from range (preserves formatting)
 * - getSpreadsheetMetadata: Get spreadsheet title and sheet list
 * - batchUpdate: Update multiple ranges in single request
 *
 * Workflow-friendly methods:
 * - getHeaders: Get column headers from first row
 * - getLastRow: Find the last row with data
 * - getRowByValue: Find a row by searching a column
 * - updateRowByValue: Update a row found by search
 * - upsertRow: Insert or update a row based on key column
 * - filterRows: Get all rows matching a condition
 * - deleteRowByValue: Delete a row found by search
 *
 * @example
 * const adapter = new GoogleSheetsAdapter()
 * const result = await adapter.call('readSheet', {
 *   spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
 *   range: 'Sheet1!A1:C10'
 * }, { accessToken: 'ya29...' })
 */
export class GoogleSheetsAdapter implements BaseIntegrationAdapter {
  readonly name = 'google-sheets'

  /**
   * Call Google Sheets API method
   * @param method - Method name (e.g., 'readSheet', 'writeSheet')
   * @param params - Method parameters
   * @param credentials - OAuth2 access token
   * @param context - Execution context for logging and tracing
   */
  async call(
    method: string,
    params: unknown,
    credentials: Record<string, unknown>,
    context?: ExecutionContext
  ): Promise<unknown> {
    // Validate credentials format
    if (!this.validateCredentials(credentials)) {
      throw new ToolingError('credentials_invalid', 'Invalid Google Sheets credentials', {
        integration: 'google-sheets',
        method
      })
    }
    const sheetsCreds = credentials as unknown as GoogleSheetsCredentials

    // Create authenticated Google Sheets client
    const sheets = await this.createClient(sheetsCreds)

    // Route to method handler
    switch (method) {
      // Core methods
      case 'readSheet':
        return this.readSheet(sheets, params as ReadSheetParams, context)
      case 'writeSheet':
        return this.writeSheet(sheets, params as WriteSheetParams, context)
      case 'appendRows':
        return this.appendRows(sheets, params as AppendRowsParams, context)
      case 'clearRange':
        return this.clearRange(sheets, params as ClearRangeParams, context)
      case 'getSpreadsheetMetadata':
        return this.getSpreadsheetMetadata(sheets, params as GetSpreadsheetMetadataParams, context)
      case 'batchUpdate':
        return this.batchUpdate(sheets, params as BatchUpdateParams, context)
      // Workflow-friendly methods
      case 'getHeaders':
        return this.getHeaders(sheets, params as GetHeadersParams, context)
      case 'getLastRow':
        return this.getLastRow(sheets, params as GetLastRowParams, context)
      case 'getRowByValue':
        return this.getRowByValue(sheets, params as GetRowByValueParams, context)
      case 'updateRowByValue':
        return this.updateRowByValue(sheets, params as UpdateRowByValueParams, context)
      case 'upsertRow':
        return this.upsertRow(sheets, params as UpsertRowParams, context)
      case 'filterRows':
        return this.filterRows(sheets, params as FilterRowsParams, context)
      case 'deleteRowByValue':
        return this.deleteRowByValue(sheets, params as DeleteRowByValueParams, context)
      default:
        throw new ToolingError('method_not_found', `Unknown method: ${method}`, {
          integration: 'google-sheets',
          method
        })
    }
  }

  /**
   * Create authenticated Google Sheets client (lazy loads @googleapis/sheets SDK)
   * CRITICAL: Use dynamic import to prevent OOM and module-level crashes
   */
  private async createClient(creds: GoogleSheetsCredentials): Promise<sheets_v4.Sheets> {
    const { sheets } = await import('@googleapis/sheets')
    const { OAuth2Client } = await import('google-auth-library')

    // Get client credentials from environment for token refresh
    const { clientId, clientSecret } = getOAuthCredentials('google-sheets')

    const oauth2Client = new OAuth2Client(clientId, clientSecret)
    oauth2Client.setCredentials({
      access_token: creds.accessToken,
      refresh_token: creds.refreshToken
    })

    return sheets({ version: 'v4', auth: oauth2Client })
  }

  /**
   * Read data from a spreadsheet range
   */
  private async readSheet(
    sheets: sheets_v4.Sheets,
    params: ReadSheetParams,
    context?: ExecutionContext
  ): Promise<ReadSheetResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.range) {
      throw new ToolingError('validation_error', 'Missing required field: range', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: params.range
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Sheet read: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} rowCount=${response.data.values?.length || 0}`
        )
      }

      return {
        values: (response.data.values as string[][]) || []
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Write data to a spreadsheet range (overwrites existing data)
   */
  private async writeSheet(
    sheets: sheets_v4.Sheets,
    params: WriteSheetParams,
    context?: ExecutionContext
  ): Promise<WriteSheetResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.range) {
      throw new ToolingError('validation_error', 'Missing required field: range', { params })
    }
    if (!params.values || !Array.isArray(params.values)) {
      throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.update({
        spreadsheetId: params.spreadsheetId,
        range: params.range,
        valueInputOption: 'USER_ENTERED',
        requestBody: { values: params.values }
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Sheet written: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedCells=${response.data.updatedCells}`
        )
      }

      return {
        updatedCells: response.data.updatedCells || 0
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Append rows to end of a table
   */
  private async appendRows(
    sheets: sheets_v4.Sheets,
    params: AppendRowsParams,
    context?: ExecutionContext
  ): Promise<AppendRowsResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.range) {
      throw new ToolingError('validation_error', 'Missing required field: range', { params })
    }
    if (!params.values || !Array.isArray(params.values)) {
      throw new ToolingError('validation_error', 'Missing required field: values (must be 2D array)', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.append({
        spreadsheetId: params.spreadsheetId,
        range: params.range,
        valueInputOption: 'USER_ENTERED',
        insertDataOption: 'INSERT_ROWS',
        requestBody: { values: params.values }
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Rows appended: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} range=${params.range} updatedRows=${response.data.updates?.updatedRows}`
        )
      }

      return {
        updatedRows: response.data.updates?.updatedRows || 0
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Clear values from a range (preserves formatting)
   */
  private async clearRange(
    sheets: sheets_v4.Sheets,
    params: ClearRangeParams,
    context?: ExecutionContext
  ): Promise<ClearRangeResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.range) {
      throw new ToolingError('validation_error', 'Missing required field: range', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.clear({
        spreadsheetId: params.spreadsheetId,
        range: params.range,
        requestBody: {} // Must be empty object, not undefined
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Range cleared: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} clearedRange=${response.data.clearedRange}`
        )
      }

      return {
        clearedRange: response.data.clearedRange || params.range
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Get spreadsheet metadata (title, sheets)
   */
  private async getSpreadsheetMetadata(
    sheets: sheets_v4.Sheets,
    params: GetSpreadsheetMetadataParams,
    context?: ExecutionContext
  ): Promise<GetSpreadsheetMetadataResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }

    try {
      const response = await sheets.spreadsheets.get({
        spreadsheetId: params.spreadsheetId,
        fields: 'properties.title,sheets.properties'
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Metadata retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} title=${response.data.properties?.title} sheetCount=${response.data.sheets?.length}`
        )
      }

      return {
        title: response.data.properties?.title || '',
        sheets:
          response.data.sheets?.map((s) => ({
            sheetId: s.properties?.sheetId ?? 0,
            title: s.properties?.title ?? '',
            index: s.properties?.index ?? 0
          })) || []
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Batch update multiple ranges in a single request
   */
  private async batchUpdate(
    sheets: sheets_v4.Sheets,
    params: BatchUpdateParams,
    context?: ExecutionContext
  ): Promise<BatchUpdateResult> {
    // Validate params
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.data || !Array.isArray(params.data) || params.data.length === 0) {
      throw new ToolingError('validation_error', 'Missing required field: data (must be non-empty array)', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.batchUpdate({
        spreadsheetId: params.spreadsheetId,
        requestBody: {
          valueInputOption: 'USER_ENTERED',
          data: params.data.map((d) => ({ range: d.range, values: d.values })),
          includeValuesInResponse: false
        }
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Batch update completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} totalUpdatedCells=${response.data.totalUpdatedCells} totalUpdatedRows=${response.data.totalUpdatedRows}`
        )
      }

      return {
        totalUpdatedCells: response.data.totalUpdatedCells || 0,
        totalUpdatedRows: response.data.totalUpdatedRows || 0
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  // ============================================
  // Workflow-friendly methods
  // ============================================

  /**
   * Get column headers from first row
   */
  private async getHeaders(
    sheets: sheets_v4.Sheets,
    params: GetHeadersParams,
    context?: ExecutionContext
  ): Promise<GetHeadersResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }

    try {
      const response = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!1:1`
      })

      const headers = (response.data.values?.[0] as string[]) || []

      // Build column map: header name -> column letter
      const columnMap: Record<string, string> = {}
      for (let i = 0; i < headers.length; i++) {
        columnMap[headers[i]] = this.indexToColumnLetter(i)
      }

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Headers retrieved: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} columnCount=${headers.length}`
        )
      }

      return {
        headers,
        columnCount: headers.length,
        columnMap
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Find the last row with data in a column
   */
  private async getLastRow(
    sheets: sheets_v4.Sheets,
    params: GetLastRowParams,
    context?: ExecutionContext
  ): Promise<GetLastRowResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }

    const column = params.column || 'A'

    try {
      const response = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!${column}:${column}`
      })

      const values = response.data.values || []
      const lastRow = values.length

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Last row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} column=${column} lastRow=${lastRow}`
        )
      }

      return {
        lastRow,
        isEmpty: lastRow === 0
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Find a row by searching for a value in a column
   */
  private async getRowByValue(
    sheets: sheets_v4.Sheets,
    params: GetRowByValueParams,
    context?: ExecutionContext
  ): Promise<GetRowByValueResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }
    if (!params.searchColumn) {
      throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
    }
    if (params.searchValue === undefined || params.searchValue === null) {
      throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
    }

    try {
      // Get headers first
      const headersResponse = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!1:1`
      })
      const headers = (headersResponse.data.values?.[0] as string[]) || []

      // Get all data in search column
      const columnResponse = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!${params.searchColumn}:${params.searchColumn}`
      })
      const columnValues = columnResponse.data.values || []

      // Find matching row (skip header row)
      let rowNumber: number | null = null
      for (let i = 1; i < columnValues.length; i++) {
        if (columnValues[i]?.[0] === params.searchValue) {
          rowNumber = i + 1 // 1-indexed
          break
        }
      }

      if (rowNumber === null) {
        if (context?.logger) {
          context.logger.info(
            `[GoogleSheetsAdapter] Row not found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} searchColumn=${params.searchColumn} searchValue=${params.searchValue}`
          )
        }
        return { found: false, rowNumber: null, rowData: null, headers }
      }

      // Get the full row data
      const rowResponse = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!${rowNumber}:${rowNumber}`
      })
      const rowData = (rowResponse.data.values?.[0] as string[]) || []

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Row found: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${rowNumber}`
        )
      }

      return { found: true, rowNumber, rowData, headers }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Update a row found by searching for a value
   */
  private async updateRowByValue(
    sheets: sheets_v4.Sheets,
    params: UpdateRowByValueParams,
    context?: ExecutionContext
  ): Promise<UpdateRowByValueResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }
    if (!params.searchColumn) {
      throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
    }
    if (params.searchValue === undefined || params.searchValue === null) {
      throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
    }
    if (!params.updates || Object.keys(params.updates).length === 0) {
      throw new ToolingError('validation_error', 'Missing required field: updates', { params })
    }

    try {
      // Find the row first
      const findResult = await this.getRowByValue(
        sheets,
        {
          spreadsheetId: params.spreadsheetId,
          sheetName: params.sheetName,
          searchColumn: params.searchColumn,
          searchValue: params.searchValue
        },
        context
      )

      if (!findResult.found || findResult.rowNumber === null) {
        return { found: false, rowNumber: null, updatedCells: 0 }
      }

      // Build the row data with updates
      const headers = findResult.headers
      const rowData = findResult.rowData || []
      const newRowData = [...rowData]

      // Extend array if needed
      while (newRowData.length < headers.length) {
        newRowData.push('')
      }

      // Apply updates by header name
      for (const [headerName, value] of Object.entries(params.updates)) {
        const colIndex = headers.indexOf(headerName)
        if (colIndex >= 0) {
          newRowData[colIndex] = value
        }
      }

      // Write back the row
      const response = await sheets.spreadsheets.values.update({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'!${findResult.rowNumber}:${findResult.rowNumber}`,
        valueInputOption: 'USER_ENTERED',
        requestBody: { values: [newRowData] }
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Row updated: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${findResult.rowNumber} updatedCells=${response.data.updatedCells}`
        )
      }

      return {
        found: true,
        rowNumber: findResult.rowNumber,
        updatedCells: response.data.updatedCells || 0
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Insert or update a row based on key column match
   */
  private async upsertRow(
    sheets: sheets_v4.Sheets,
    params: UpsertRowParams,
    context?: ExecutionContext
  ): Promise<UpsertRowResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }
    if (!params.keyColumn) {
      throw new ToolingError('validation_error', 'Missing required field: keyColumn', { params })
    }
    if (params.keyValue === undefined || params.keyValue === null) {
      throw new ToolingError('validation_error', 'Missing required field: keyValue', { params })
    }
    if (!params.rowData || Object.keys(params.rowData).length === 0) {
      throw new ToolingError('validation_error', 'Missing required field: rowData', { params })
    }

    try {
      // Try to find existing row
      const findResult = await this.getRowByValue(
        sheets,
        {
          spreadsheetId: params.spreadsheetId,
          sheetName: params.sheetName,
          searchColumn: params.keyColumn,
          searchValue: params.keyValue
        },
        context
      )

      const headers = findResult.headers

      if (findResult.found && findResult.rowNumber !== null) {
        // Update existing row
        const updateResult = await this.updateRowByValue(
          sheets,
          {
            spreadsheetId: params.spreadsheetId,
            sheetName: params.sheetName,
            searchColumn: params.keyColumn,
            searchValue: params.keyValue,
            updates: params.rowData
          },
          context
        )

        return {
          action: 'updated',
          rowNumber: findResult.rowNumber,
          updatedCells: updateResult.updatedCells
        }
      } else {
        // Insert new row - build row array from headers
        const newRow: string[] = headers.map((h) => params.rowData[h] || '')

        // Get last row to know where to append
        const lastRowResult = await this.getLastRow(
          sheets,
          {
            spreadsheetId: params.spreadsheetId,
            sheetName: params.sheetName,
            column: 'A'
          },
          context
        )

        const newRowNumber = lastRowResult.lastRow + 1

        const response = await sheets.spreadsheets.values.update({
          spreadsheetId: params.spreadsheetId,
          range: `'${params.sheetName}'!${newRowNumber}:${newRowNumber}`,
          valueInputOption: 'USER_ENTERED',
          requestBody: { values: [newRow] }
        })

        if (context?.logger) {
          context.logger.info(
            `[GoogleSheetsAdapter] Row inserted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} rowNumber=${newRowNumber}`
          )
        }

        return {
          action: 'inserted',
          rowNumber: newRowNumber,
          updatedCells: response.data.updatedCells || 0
        }
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Get all rows matching a filter condition
   */
  private async filterRows(
    sheets: sheets_v4.Sheets,
    params: FilterRowsParams,
    context?: ExecutionContext
  ): Promise<FilterRowsResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }
    if (!params.filterColumn) {
      throw new ToolingError('validation_error', 'Missing required field: filterColumn', { params })
    }
    if (params.filterValue === undefined || params.filterValue === null) {
      throw new ToolingError('validation_error', 'Missing required field: filterValue', { params })
    }

    const operator = params.operator || 'equals'

    try {
      // Get all data
      const response = await sheets.spreadsheets.values.get({
        spreadsheetId: params.spreadsheetId,
        range: `'${params.sheetName}'`
      })

      const allData = (response.data.values as string[][]) || []
      if (allData.length === 0) {
        return { rows: [], matchCount: 0, headers: [] }
      }

      const headers = allData[0]
      const colIndex = this.columnLetterToIndex(params.filterColumn)

      // Filter rows (skip header)
      const matchingRows: Array<{ rowNumber: number; data: string[] }> = []

      for (let i = 1; i < allData.length; i++) {
        const cellValue = allData[i]?.[colIndex] || ''
        let matches = false

        switch (operator) {
          case 'equals':
            matches = cellValue === params.filterValue
            break
          case 'contains':
            matches = cellValue.includes(params.filterValue)
            break
          case 'startsWith':
            matches = cellValue.startsWith(params.filterValue)
            break
          case 'endsWith':
            matches = cellValue.endsWith(params.filterValue)
            break
        }

        if (matches) {
          matchingRows.push({
            rowNumber: i + 1, // 1-indexed
            data: allData[i]
          })
        }
      }

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Filter completed: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} matchCount=${matchingRows.length}`
        )
      }

      return {
        rows: matchingRows,
        matchCount: matchingRows.length,
        headers
      }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Delete a row by searching for a value
   */
  private async deleteRowByValue(
    sheets: sheets_v4.Sheets,
    params: DeleteRowByValueParams,
    context?: ExecutionContext
  ): Promise<DeleteRowByValueResult> {
    if (!params.spreadsheetId) {
      throw new ToolingError('validation_error', 'Missing required field: spreadsheetId', { params })
    }
    if (!params.sheetName) {
      throw new ToolingError('validation_error', 'Missing required field: sheetName', { params })
    }
    if (!params.searchColumn) {
      throw new ToolingError('validation_error', 'Missing required field: searchColumn', { params })
    }
    if (params.searchValue === undefined || params.searchValue === null) {
      throw new ToolingError('validation_error', 'Missing required field: searchValue', { params })
    }

    try {
      // Find the row first
      const findResult = await this.getRowByValue(
        sheets,
        {
          spreadsheetId: params.spreadsheetId,
          sheetName: params.sheetName,
          searchColumn: params.searchColumn,
          searchValue: params.searchValue
        },
        context
      )

      if (!findResult.found || findResult.rowNumber === null) {
        return { found: false, deletedRowNumber: null }
      }

      // Get sheet ID for the delete request
      const metadataResponse = await sheets.spreadsheets.get({
        spreadsheetId: params.spreadsheetId,
        fields: 'sheets.properties'
      })

      const sheetInfo = metadataResponse.data.sheets?.find((s) => s.properties?.title === params.sheetName)

      if (!sheetInfo?.properties || sheetInfo.properties.sheetId === undefined) {
        throw new ToolingError('api_error', `Sheet not found: ${params.sheetName}`, {
          spreadsheetId: params.spreadsheetId,
          sheetName: params.sheetName
        })
      }

      // Delete the row using batchUpdate
      await sheets.spreadsheets.batchUpdate({
        spreadsheetId: params.spreadsheetId,
        requestBody: {
          requests: [
            {
              deleteDimension: {
                range: {
                  sheetId: sheetInfo.properties.sheetId,
                  dimension: 'ROWS',
                  startIndex: findResult.rowNumber - 1, // 0-indexed
                  endIndex: findResult.rowNumber // exclusive
                }
              }
            }
          ]
        }
      })

      if (context?.logger) {
        context.logger.info(
          `[GoogleSheetsAdapter] Row deleted: organizationId=${context.organizationId} executionId=${context.executionId} spreadsheetId=${params.spreadsheetId} sheetName=${params.sheetName} deletedRowNumber=${findResult.rowNumber}`
        )
      }

      return { found: true, deletedRowNumber: findResult.rowNumber }
    } catch (error: any) {
      throw this.handleApiError(error, params.spreadsheetId)
    }
  }

  /**
   * Convert column letter to 0-based index (A=0, B=1, ..., Z=25, AA=26, etc.)
   */
  private columnLetterToIndex(letter: string): number {
    let index = 0
    for (let i = 0; i < letter.length; i++) {
      index = index * 26 + (letter.charCodeAt(i) - 64)
    }
    return index - 1
  }

  /**
   * Convert 0-based index to column letter (0=A, 1=B, ..., 25=Z, 26=AA, etc.)
   */
  private indexToColumnLetter(index: number): string {
    let letter = ''
    let temp = index
    while (temp >= 0) {
      letter = String.fromCharCode((temp % 26) + 65) + letter
      temp = Math.floor(temp / 26) - 1
    }
    return letter
  }

  /**
   * Handle Google Sheets API errors with meaningful messages
   */
  private handleApiError(error: any, spreadsheetId: string): ToolingError {
    const code = error.code || error.status || error.response?.status

    if (code === 404) {
      return new ToolingError('api_error', `Spreadsheet not found: ${spreadsheetId}`, {
        spreadsheetId,
        statusCode: 404
      })
    }
    if (code === 403) {
      return new ToolingError('permission_denied', 'Access denied. Check sharing permissions.', {
        spreadsheetId,
        statusCode: 403
      })
    }
    if (code === 401) {
      return new ToolingError('credentials_invalid', 'Authentication expired. Token refresh required.', {
        spreadsheetId,
        statusCode: 401
      })
    }
    if (code === 429) {
      return new ToolingError('rate_limit_exceeded', 'Rate limited. Retry after delay.', {
        spreadsheetId,
        statusCode: 429
      })
    }

    return new ToolingError('api_error', `Google Sheets API error: ${error.message}`, {
      spreadsheetId,
      statusCode: code,
      details: error
    })
  }

  /**
   * Validate credentials structure
   * Required by BaseIntegrationAdapter interface
   */
  validateCredentials(creds: Record<string, unknown>): boolean {
    return typeof creds.accessToken === 'string' && creds.accessToken.length > 0
  }
}
