import crypto from 'crypto'
import {
    AppCredentialType,
    ConnectionKey,
    ConnectionKeyId,
    GetOrDeleteConnectionFromTokenRequest,
    UpsertApiKeyConnectionFromToken,
    UpsertConnectionFromToken,
    UpsertOAuth2ConnectionFromToken,
    UpsertSigningKeyConnection,
} from '@activepieces/ee-shared'
import {
    ActivepiecesError, apId,
    AppConnectionScope,
    AppConnectionType,
    AppConnectionWithoutSensitiveData,
    Cursor,
    ErrorCode,
    isNil,
    ProjectId,
    SeekPage,
} from '@activepieces/shared'
import { FastifyBaseLogger } from 'fastify'
import jsonwebtoken from 'jsonwebtoken'
import { appConnectionService } from '../../app-connection/app-connection-service/app-connection-service'
import { repoFactory } from '../../core/db/repo-factory'
import { buildPaginator } from '../../helper/pagination/build-paginator'
import { paginationHelper } from '../../helper/pagination/pagination-utils'
import { projectService } from '../../project/project-service'
import { appCredentialService } from '../app-credentials/app-credentials.service'
import { ConnectionKeyEntity } from './connection-key.entity'

const connectionKeyRepo = repoFactory(ConnectionKeyEntity)

export const connectionKeyService = (log: FastifyBaseLogger) => ({
    async getConnection({
        projectId,
        token,
        appName,
    }: GetOrDeleteConnectionFromTokenRequest): Promise<AppConnectionWithoutSensitiveData | null> {
        const connectionName = await getConnectioName({ projectId, token })
        const project = await projectService.getOneOrThrow(projectId)

        // TODO this is hardcoded for now, just to make sure it's not changed on client side
        const finalAppName = appName.replace('@activepieces/piece-', '')
        if (connectionName == null) {
            throw new ActivepiecesError({
                code: ErrorCode.INVALID_OR_EXPIRED_JWT_TOKEN,
                params: {
                    token,
                },
            })
        }
        const connection = await appConnectionService(log).getOne({
            projectId,
            platformId: project.platformId,
            externalId: `${finalAppName}_${connectionName}`,
        })
        return isNil(connection) ? null : appConnectionService(log).removeSensitiveData(connection)
    },
    async createConnection(
        request: UpsertConnectionFromToken,
    ): Promise<AppConnectionWithoutSensitiveData> {
        const appCredential = await appCredentialService.getOneOrThrow(
            request.appCredentialId,
        )
        const projectId = appCredential.projectId
        const project = await projectService.getOneOrThrow(projectId)
        
        const connectionName = await getConnectioName({
            projectId,
            token: request.token,
        })
        if (connectionName == null) {
            throw new ActivepiecesError({
                code: ErrorCode.INVALID_OR_EXPIRED_JWT_TOKEN,
                params: {
                    token: request.token,
                },
            })
        }

        // TODO this is hardcoded for now, just to make sure it's not changed on client side
        const finalAppName = `@activepieces/piece-${appCredential.appName}`
        switch (appCredential.settings.type) {
            case AppCredentialType.API_KEY: {
                const apiRequest = request as UpsertApiKeyConnectionFromToken
                return appConnectionService(log).upsert({
                    scope: AppConnectionScope.PROJECT,
                    platformId: project.platformId,
                    projectIds: [projectId],
                    externalId: `${appCredential.appName}_${connectionName}`,
                    displayName: `${appCredential.appName}_${connectionName}`,
                    pieceName: finalAppName,
                    type: AppConnectionType.SECRET_TEXT,
                    value: {
                        type: AppConnectionType.SECRET_TEXT,
                        secret_text: apiRequest.apiKey,
                    },
                    ownerId: null,
                })
            }
            case AppCredentialType.OAUTH2: {
                const apiRequest = request as UpsertOAuth2ConnectionFromToken
                return appConnectionService(log).upsert({
                    scope: AppConnectionScope.PROJECT,
                    platformId: project.platformId,
                    projectIds: [projectId],
                    externalId: `${appCredential.appName}_${connectionName}`,
                    displayName: `${appCredential.appName}_${connectionName}`,
                    pieceName: finalAppName,
                    type: AppConnectionType.OAUTH2,
                    value: {
                        type: AppConnectionType.OAUTH2,
                        redirect_url: apiRequest.redirectUrl,
                        code: apiRequest.code,
                        props: apiRequest.props,
                        scope: appCredential.settings.scope,
                        client_id: appCredential.settings.clientId,
                        client_secret: appCredential.settings.clientSecret!,
                    },
                    ownerId: null,
                })
            }
        }
    },
    async upsert({
        projectId,
        request,
    }: {
        projectId: ProjectId
        request: UpsertSigningKeyConnection
    }): Promise<ConnectionKey> {
        const key = crypto.generateKeyPairSync('rsa', {
            modulusLength: 2048,
            publicKeyEncoding: {
                type: 'pkcs1',
                format: 'pem',
            },
            privateKeyEncoding: {
                type: 'pkcs1',
                format: 'pem',
            },
        })
        const savedConnection: ConnectionKey = await connectionKeyRepo().save({
            id: apId(),
            projectId,
            settings: {
                type: request.settings.type,
                publicKey: key.publicKey,
            },
        })
        return {
            ...savedConnection,
            settings: {
                type: savedConnection.settings.type,
                publicKey: savedConnection.settings.publicKey,
                privateKey: key.privateKey,
            },
        }
    },
    async list(
        projectId: ProjectId,
        cursorRequest: Cursor | null,
        limit: number,
    ): Promise<SeekPage<ConnectionKey>> {
        const decodedCursor = paginationHelper.decodeCursor(cursorRequest ?? null)
        const paginator = buildPaginator({
            entity: ConnectionKeyEntity,
            query: {
                limit,
                order: 'ASC',
                afterCursor: decodedCursor.nextCursor,
                beforeCursor: decodedCursor.previousCursor,
            },
        })
        const queryBuilder = connectionKeyRepo()
            .createQueryBuilder('connection_key')
            .where({ projectId })
        const { data, cursor } = await paginator.paginate(queryBuilder)
        return paginationHelper.createPage<ConnectionKey>(data, cursor)
    },
    async delete(id: ConnectionKeyId): Promise<void> {
        await connectionKeyRepo().delete({
            id,
        })
    },
})

async function getConnectioName(request: {
    projectId: string
    token: string
}): Promise<string | null> {
    const connectionKeys = await connectionKeyRepo().findBy({
        projectId: request.projectId,
    })
    let connectionName: string | null = null
    for (let i = 0; i < connectionKeys.length; ++i) {
        const currentKey = connectionKeys[i]
        const decodedTokenSub = decodeTokenOrNull(
            request.token,
            currentKey.settings.publicKey,
        )?.sub
        if (decodedTokenSub !== null && decodedTokenSub !== undefined) {
            connectionName = decodedTokenSub as string
            break
        }
    }
    return connectionName
}

function decodeTokenOrNull(token: string, publicKey: string) {
    try {
        return jsonwebtoken.verify(token, publicKey!)
    }
    catch (e) {
        return null
    }
}
