import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/pikku-types.gen.js'
import type { DB } from '@heygermany/sdk'
import {
  ApplicantStatuses,
  ApplicationCandidateData,
  CandidateDocumentTypes,
  UserRoles,
  UserTypes,
} from '@heygermany/sdk'
import { PickRequired } from '@pikku/core'
import { NotFoundError, ConflictError } from '@pikku/core/errors'
import { isBackOffice, isCandidate } from '../../permissions.js'
import { copyBetterAuthCookies, forwardedAuthHeaders, getWebsiteOrigin } from '../../better-auth.js'
import { toBucketKey } from '../../content-path.js'
import { normalizeStringArray } from '../../normalizers.js'

const createCandidateCredential = () => `${crypto.randomUUID()}-${crypto.randomUUID()}`

export const startFlow = pikkuSessionlessFunc<
  { name: string, email: string },
  void
>({
  auth: false,
  title: 'Start user flow',
  description: 'Creates a new user and sets up session',
  func: async ({ kysely, auth, config }, { name, email }, { http }) => {
    if (!http) {
      throw new Error('Candidate signup requires an HTTP request context')
    }

    const existingAuthUser = await kysely
      .selectFrom('authUser')
      .select('id')
      .where('email', '=', email)
      .executeTakeFirst()

    if (existingAuthUser) {
      throw new ConflictError('Email already exists')
    }

    const authApi = await auth()
    const signUpResponse = await authApi.api.signUpEmail({
      body: {
        name,
        email,
        password: createCandidateCredential(),
        role: UserRoles.OWNER,
        type: UserTypes.CANDIDATE,
      },
      headers: forwardedAuthHeaders(http),
      asResponse: true,
    })

    if (!signUpResponse.ok) {
      if (signUpResponse.status === 409 || signUpResponse.status === 422) {
        throw new ConflictError('Email already exists')
      }

      throw new Error(await signUpResponse.text() || 'Failed to create Better Auth user')
    }

    const signUpData = await signUpResponse.json() as {
      user: {
        id: string
      }
    }
    const userId = signUpData.user.id

    try {
      await kysely.transaction().execute(async (transaction) => {
        // Check if candidate with this email already exists
        const existingCandidate = await transaction
          .selectFrom('candidate')
          .select('candidateId')
          .where('email', '=', email)
          .executeTakeFirst()

        if (existingCandidate) {
          throw new ConflictError('Email already exists')
        }

        // Create user first
        await transaction
          .insertInto('user')
          .values({
            userId,
            name,
            email,
            role: UserRoles.OWNER,
            type: UserTypes.CANDIDATE
          })
          .execute()

        // Create candidate record
        await transaction
          .insertInto('candidate')
          .values({ name, email, candidateId: userId })
          .returning('candidateId')
          .executeTakeFirstOrThrow()
      })
    } catch (error) {
      await kysely
        .deleteFrom('authUser')
        .where('id', '=', userId)
        .execute()

      throw error
    }

    copyBetterAuthCookies(signUpResponse, http)

    await authApi.api.sendVerificationEmail({
      body: {
        email,
        callbackURL: `${getWebsiteOrigin(config.domain)}/verify-email?verified=1`,
      },
      headers: forwardedAuthHeaders(http),
    })
  }
})

export const getCandidate = pikkuFunc<
  void,
  ApplicationCandidateData
>({
  permissions: {
    isBackOffice,
    isCandidate
  },
  title: 'Get candidate data',
  description: 'Fetches candidate form data for the authenticated user',
  errors: [NotFoundError],
  func: async (_services, _, { session, rpc }) => {
    if (!session?.userId) {
      throw new NotFoundError('Candidate not found')
    }

    return await rpc.invoke('getCandidateById', { candidateId: session.userId })
  }
})

export const getCandidateById = pikkuFunc<
  { candidateId: string },
  ApplicationCandidateData
>({
  expose: true,
  permissions: {
    isBackOffice,
    isCandidate
  },
  func: async ({ kysely }, { candidateId }) => {
    const candidate = await kysely
      .selectFrom('candidate')
      .select(['candidateId', 'dateOfBirth', 'languageCertificateProvided', 'licenseProvided', 'name', 'submittedAt', 'status', 'stateWorkPreference', 'joinTalentPool', 'livingInGermany', 'euPassport', 'countryEducation', 'languageSelfAssessmentLevel', 'languagesSpoken', 'nationality', 'currentResidence', 'professionalRecognitionStatus', 'qualificationStatusGermany', 'recognitionLikelihood'])
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))

    // Get documents by category
    const documents = await kysely
      .selectFrom('candidateDocument')
      .selectAll()
      .where('candidateId', '=', candidateId)
      .execute()

    // Group documents by category
    const education = documents.filter(d => d.category === CandidateDocumentTypes.EDUCATION)
    const licenses = documents.filter(d => d.category === CandidateDocumentTypes.LICENSE)
    const languageCertificates = documents.filter(d => d.category === CandidateDocumentTypes.LANGUAGE)
    const workExperience = documents.filter(d => d.category === CandidateDocumentTypes.WORK_EXPERIENCE)

    return {
      ...candidate,
      stateWorkPreference: normalizeStringArray(candidate.stateWorkPreference),
      languagesSpoken: normalizeStringArray(candidate.languagesSpoken),
      education,
      licenses,
      languageCertificates,
      workExperience,
    }
  }
})

export const updateCandidate = pikkuFunc<
  PickRequired<Partial<DB.Candidate>, 'candidateId'>,
  void
>({
  expose: true,
  title: 'Update user candidate data',
  description: 'Updates candidate form data for the authenticated user',
  permissions: {
    isBackOffice,
    isCandidate
  },
  func: async ({ kysely }, data) => {
    const currentCandidate = await kysely
      .selectFrom('candidate')
      .select('status')
      .where('candidateId', '=', data.candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError())

    const shouldResetToInitial =
      currentCandidate.status === ApplicantStatuses.INVALID &&
      data.status === ApplicantStatuses.INITIAL

    const shouldMoveToPending =
      currentCandidate.status === ApplicantStatuses.INITIAL &&
      !!data.submittedAt

    const nextStatus = shouldResetToInitial
      ? ApplicantStatuses.INITIAL
      : shouldMoveToPending
        ? ApplicantStatuses.PENDING
        : undefined

    await kysely
      .updateTable('candidate')
      .set({
        stateWorkPreference: data.stateWorkPreference,
        joinTalentPool: data.joinTalentPool,
        livingInGermany: data.livingInGermany,
        euPassport: data.euPassport,
        countryEducation: data.countryEducation,
        currentResidence: data.currentResidence,
        dateOfBirth: data.dateOfBirth,
        nationality: data.nationality,
        professionalRecognitionStatus: data.professionalRecognitionStatus,
        qualificationStatusGermany: data.qualificationStatusGermany,
        recognitionLikelihood: data.recognitionLikelihood,
        languagesSpoken: data.languagesSpoken,
        languageSelfAssessmentLevel: data.languageSelfAssessmentLevel,
        submittedAt: shouldResetToInitial ? null : data.submittedAt,
        status: nextStatus,
      })
      .where('candidateId', '=', data.candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError())
  }
})

export const addCandidateDocuments = pikkuFunc<
  {
    candidateId: string,
    documents: Array<{
      contentType: string
      size: number
      category: DB.CandidateDocumentType
      fileName: string,
    }>
  },
  Array<{
    documentId: string
    uploadUrl: string
    assetKey: string
  }>
>({
  expose: true,
  permissions: {
    isBackOffice,
    isCandidate
  },
  title: 'Adds an candidate document that has been uploaded',
  func: async ({ kysely, content }, { candidateId, documents }) => {
    const documentIds = documents.map(_document => crypto.randomUUID())

    const getAssetKey = (category: DB.CandidateDocumentType, documentId: string, contentType: string) =>
      `candidate/${candidateId}/${category}/${documentId}.${contentType.split('/')[1]}`

    const insertedDocuments = await kysely
      .insertInto('candidateDocument')
      .values(documents.map((document, index) => {
        const { contentType, size, category } = document
        return {
          candidateId,
          documentId: documentIds[index],
          mimeType: contentType,
          fileSize: size,
          category: category as any,
          fileName: `${document.fileName}.${contentType.split('/')[1]}`,
          filePath: getAssetKey(category as any, documentIds[index], contentType),
        }
      }))
      .returning('documentId')
      .execute()

    return await Promise.all(documents.map(async (document, index) => {
      const { contentType, category } = document
      const filePath = getAssetKey(category as any, documentIds[index], contentType)
      const { bucket, key } = toBucketKey(filePath)

      return {
        ...await content.getUploadURL({
          bucket,
          fileKey: key,
          contentType,
        }),
        documentId: insertedDocuments[index].documentId
      }
    }))
  }
})

export const updateCandidateDocument = pikkuFunc<{ candidateId: string, documentId: string, uploaded?: true }, void>({
  expose: true,
  permissions: {
    isBackOffice,
    isCandidate
  },
  title: 'Update candidate document',
  description: 'Updates a document uploaded by the candidate and creates entry in relevant table',
  func: async ({ kysely }, { candidateId, documentId, uploaded }) => {
    await kysely.transaction().execute(async (transaction) => {
      if (uploaded) {
        const document = await transaction
          .updateTable('candidateDocument')
          .set('uploaded', uploaded)
          .where('candidateDocument.documentId', '=', documentId)
          .where('candidateDocument.candidateId', '=', candidateId)
          .returning(['category'])
          .executeTakeFirstOrThrow(() => new NotFoundError('Document not found'))

        const { category } = document

        switch (category) {
          case CandidateDocumentTypes.EDUCATION:
            await transaction
              .insertInto('candidateEducation')
              .values({
                educationId: crypto.randomUUID(),
                candidateId,
              })
              .onConflict((oc) => oc.column('candidateId').doNothing())
              .execute()
            break

          case CandidateDocumentTypes.LICENSE:
            await transaction
              .insertInto('candidateLicense')
              .values({
                licenseId: crypto.randomUUID(),
                candidateId,
              })
              .onConflict((oc) => oc.column('candidateId').doNothing())
              .execute()
            break

          case CandidateDocumentTypes.LANGUAGE:
            await transaction
              .insertInto('candidateLanguage')
              .values({
                languageId: crypto.randomUUID(),
                candidateId,
              })
              .onConflict((oc) => oc.column('candidateId').doNothing())
              .execute()
            break

          case CandidateDocumentTypes.WORK_EXPERIENCE:
            await transaction
              .insertInto('candidateExperience')
              .values({
                experienceId: crypto.randomUUID(),
                candidateId,
              })
              .execute()
            break
        }
      }
    })
  }
})

export const deleteCandidateDocument = pikkuFunc<{ candidateId: string, documentId: string }, void>({
  expose: true,
  title: 'Delete candidate document',
  description: 'Deletes a document uploaded by the candidate',
  permissions: {
    isBackOffice,
    isCandidate
  },
  func: async ({ kysely, content, logger }, { candidateId, documentId }) => {
    await kysely.transaction().execute(async (transaction) => {
      const document = await transaction
        .deleteFrom('candidateDocument')
        .where('candidateDocument.documentId', '=', documentId)
        .where('candidateDocument.candidateId', '=', candidateId)
        .returningAll()
        .executeTakeFirstOrThrow(() => new NotFoundError('Document not found'))
      const deleted = await content.deleteFile(toBucketKey(document.filePath))
      if (!deleted) {
        logger.error('Failed to delete file from storage', { filePath: document.filePath })
      }
    })
  }
})
