import type { BaseIntegrationAdapter } from '../../../base-integration-adapter'
import { ToolingError } from '../../../../types'
import type { ExecutionContext } from '../../../../../base/types'
import { refreshOAuthTokenIfExpired, forceRefreshOAuthToken } from '../../../../../../../integrations/oauth/server/refresh'
import type { OAuthToken } from '../../../../../../../integrations/oauth/types'
import { getToolServices } from '../../../../registry'
import { uploadFile } from './fetch/upload-file'
import type { UploadFileParams } from './fetch/upload-file'
import { createFolder } from './fetch/create-folder'
import type { CreateFolderParams } from './fetch/create-folder'

/**
 * Dropbox credentials structure (OAuth token)
 */
export interface DropboxCredentials {
  accessToken: string
  refreshToken?: string
  expiresAt?: string
  provider?: string
}

/**
 * Dropbox integration adapter
 *
 * Provides access to Dropbox for file storage operations.
 * Uses raw fetch (no SDK) with OAuth token refresh.
 *
 * @example
 * ```typescript
 * const adapter = new DropboxAdapter()
 * const result = await adapter.call(
 *   'uploadFile',
 *   { path: '/folder/file.pdf', contents: buffer },
 *   { accessToken: 'sl.xxx', refreshToken: 'xxx' },
 *   context
 * )
 * ```
 */
export class DropboxAdapter implements BaseIntegrationAdapter {
  readonly name = 'dropbox'

  /**
   * Call Dropbox method with automatic token refresh
   */
  async call(
    method: string,
    params: unknown,
    credentials: Record<string, unknown>,
    context?: ExecutionContext
  ): Promise<unknown> {
    // Validate credentials first
    if (!this.validateCredentials(credentials)) {
      throw new ToolingError('credentials_invalid', 'Invalid Dropbox credentials', {
        integration: this.name,
        organizationId: context?.organizationId
      })
    }

    // Cast to typed credentials
    let creds = credentials as unknown as DropboxCredentials

    // Refresh token if expired (OAuth tokens expire in 4 hours)
    if (creds.refreshToken && creds.expiresAt && creds.provider) {
      context?.logger?.debug(
        `Dropbox token refresh check: provider=${creds.provider}, expiresAt=${creds.expiresAt}, tokenPrefix=${creds.accessToken?.substring(0, 10)}`
      )

      const token: OAuthToken = {
        accessToken: creds.accessToken,
        refreshToken: creds.refreshToken,
        expiresAt: creds.expiresAt,
        provider: creds.provider,
        tokenType: 'Bearer'
      }

      const refreshedToken = await refreshOAuthTokenIfExpired(token)

      const wasRefreshed = refreshedToken.accessToken !== creds.accessToken
      context?.logger?.debug(
        `Dropbox token refresh result: wasRefreshed=${wasRefreshed}, oldTokenPrefix=${creds.accessToken.substring(0, 10)}, newTokenPrefix=${refreshedToken.accessToken.substring(0, 10)}`
      )

      // If token was refreshed, persist updated token to database
      if (wasRefreshed) {
        const { credentialsService } = getToolServices()

        if (!credentialsService) {
          context?.logger?.error('CredentialsService not available for Dropbox token persistence')
        } else if (!context?.organizationId) {
          context?.logger?.error('organizationId missing from ExecutionContext for Dropbox token persistence')
        } else if (!context.credentialName) {
          // Cannot persist token without credential name
          context.logger?.error(
            'Cannot persist refreshed Dropbox token: credentialName missing from ExecutionContext'
          )
        } else {
          // Update credential with new token (non-blocking)
          // Note: credentialsService.updateCredential is available via the service implementation
          // but not exposed in ICredentialsService interface - using type assertion through unknown
          const service = credentialsService as unknown as {
            updateCredential: (
              organizationId: string,
              credentialName: string,
              value: Record<string, unknown>
            ) => Promise<void>
          }

          context.logger?.info(
            `Persisting refreshed Dropbox token: credentialName=${context.credentialName}, orgId=${context.organizationId}, provider=${refreshedToken.provider}, newExpiresAt=${refreshedToken.expiresAt}`
          )

          service
            .updateCredential(context.organizationId, context.credentialName, {
              accessToken: refreshedToken.accessToken,
              refreshToken: refreshedToken.refreshToken,
              expiresAt: refreshedToken.expiresAt,
              provider: refreshedToken.provider,
              tokenType: refreshedToken.tokenType
            })
            .then(() => {
              context.logger?.info(
                `Dropbox token refreshed and persisted successfully: credentialName=${context.credentialName}, newExpiresAt=${refreshedToken.expiresAt}`
              )
            })
            .catch((err: Error) => {
              context.logger?.error(
                `Failed to persist refreshed Dropbox token: credentialName=${context.credentialName}, error=${err.message}`
              )
            })
        }
        creds = {
          ...creds,
          accessToken: refreshedToken.accessToken
        }
      }
    } else {
      context?.logger?.warn(
        `Dropbox token refresh skipped - missing required OAuth fields: hasRefreshToken=${!!creds.refreshToken}, hasExpiresAt=${!!creds.expiresAt}, hasProvider=${!!creds.provider}`
      )
    }

    // Route to method handler with 401 retry logic
    try {
      return await this.executeMethod(method, params, creds, context)
    } catch (error) {
      // Check if this is a 401 credentials_invalid error that we can retry
      if (
        error instanceof ToolingError &&
        error.errorType === 'credentials_invalid' &&
        creds.refreshToken &&
        creds.provider
      ) {
        context?.logger?.warn(
          'Dropbox API returned 401 - attempting force refresh and retry'
        )

        // Force refresh the token (bypass expiry check)
        const token: OAuthToken = {
          accessToken: creds.accessToken,
          refreshToken: creds.refreshToken,
          expiresAt: creds.expiresAt || new Date().toISOString(),
          provider: creds.provider,
          tokenType: 'Bearer'
        }

        try {
          const refreshedToken = await forceRefreshOAuthToken(token)

          context?.logger?.info(
            `Dropbox token force-refreshed: newTokenPrefix=${refreshedToken.accessToken.substring(0, 10)}, newExpiresAt=${refreshedToken.expiresAt}`
          )

          // Persist the refreshed token
          await this.persistToken(refreshedToken, context)

          // Update credentials and retry
          const newCreds: DropboxCredentials = {
            ...creds,
            accessToken: refreshedToken.accessToken,
            expiresAt: refreshedToken.expiresAt
          }

          context?.logger?.info('Retrying Dropbox API call with refreshed token')
          return await this.executeMethod(method, params, newCreds, context)
        } catch (refreshError) {
          // If refresh fails, throw original error with additional context
          const refreshMsg = refreshError instanceof Error ? refreshError.message : String(refreshError)
          context?.logger?.error(`Dropbox token force-refresh failed: ${refreshMsg}`)

          if (refreshMsg.includes('invalid_grant')) {
            throw new ToolingError(
              'credentials_invalid',
              'Dropbox refresh token is invalid or revoked. User must re-authorize via OAuth.',
              { integration: this.name, refreshError: refreshMsg }
            )
          }
          throw error // Re-throw original 401 error
        }
      }

      // Not a retryable error, re-throw
      throw error
    }
  }

  /**
   * Execute the actual Dropbox method
   */
  private async executeMethod(
    method: string,
    params: unknown,
    creds: DropboxCredentials,
    context?: ExecutionContext
  ): Promise<unknown> {
    switch (method) {
      case 'uploadFile':
        return uploadFile(creds, params as UploadFileParams, context)
      case 'createFolder':
        return createFolder(creds, params as CreateFolderParams, context)
      default:
        throw new ToolingError('method_not_found', `Unknown Dropbox method: ${method}`, {
          integration: this.name,
          method,
          organizationId: context?.organizationId
        })
    }
  }

  /**
   * Persist refreshed token to database
   */
  private async persistToken(token: OAuthToken, context?: ExecutionContext): Promise<void> {
    const { credentialsService } = getToolServices()

    if (!credentialsService) {
      context?.logger?.error('CredentialsService not available for Dropbox token persistence')
      return
    }
    if (!context?.organizationId) {
      context?.logger?.error('organizationId missing from ExecutionContext for Dropbox token persistence')
      return
    }
    if (!context.credentialName) {
      context?.logger?.error('credentialName missing from ExecutionContext for Dropbox token persistence')
      return
    }

    const service = credentialsService as unknown as {
      updateCredential: (
        organizationId: string,
        credentialName: string,
        value: Record<string, unknown>
      ) => Promise<void>
    }

    try {
      await service.updateCredential(context.organizationId, context.credentialName, {
        accessToken: token.accessToken,
        refreshToken: token.refreshToken,
        expiresAt: token.expiresAt,
        provider: token.provider,
        tokenType: token.tokenType
      })
      context.logger?.info(
        `Dropbox token persisted: credentialName=${context.credentialName}, newExpiresAt=${token.expiresAt}`
      )
    } catch (err) {
      const errMsg = err instanceof Error ? err.message : String(err)
      context?.logger?.error(`Failed to persist Dropbox token: ${errMsg}`)
    }
  }

  /**
   * Validate Dropbox credentials
   */
  validateCredentials(credentials: Record<string, unknown>): boolean {
    const creds = credentials as unknown as DropboxCredentials
    // Access token should be a non-empty string (Dropbox tokens start with 'sl.')
    return !!creds.accessToken && typeof creds.accessToken === 'string' && creds.accessToken.length > 0
  }
}
