import { pikkuFunc } from '#pikku/pikku-types.gen.js'
import type { DB } from '@heygermany/sdk'
import {
  ApplicantStatuses,
  BackOfficeCandidate,
  GetBackOfficeCandidatesOutput,
  BackOfficeCandidateListItem,
  CandidateDocumentTypes,
  CandidateLifecycleNotifications,
} from '@heygermany/sdk'
import { NotFoundError } from '@pikku/core/errors'
import dayjs from 'dayjs'
import { isBackOffice } from '../../permissions.js'
import { forwardedAuthHeaders } from '../../better-auth.js'
import { toBucketKey } from '../../content-path.js'
import { normalizeStringArray } from '../../normalizers.js'

const omitLifecycleNotifications = <T extends Record<string, unknown>>(candidate: T): Omit<T, 'lifecycleNotifications'> => {
  const {
    lifecycleNotifications: _lifecycleNotifications,
    ...candidateWithoutLifecycleNotifications
  } = candidate as T & { lifecycleNotifications?: unknown }

  return candidateWithoutLifecycleNotifications as Omit<T, 'lifecycleNotifications'>
}

export const getBackOfficeSignedCandidateDocument = pikkuFunc<
  { candidateId: string, documentId: string },
  { url: string }
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Get signed candidate document',
  description: 'Fetches the signed URL for a candidate document',
  func: async ({ kysely, content }, { candidateId, documentId }) => {
    const document = await kysely
      .selectFrom('candidateDocument')
      .selectAll()
      .where('documentId', '=', documentId)
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Document not found'))
    const { bucket, key } = toBucketKey(document.filePath)

    return {
      url: await content.signContentKey({
        bucket,
        contentKey: key,
        dateLessThan: dayjs().add(15, 'minutes').toDate(),
      })
    }
  }
})

export const getBackOfficeCandidates = pikkuFunc<
  { offset?: number; limit?: number },
  GetBackOfficeCandidatesOutput
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Get all candidates',
  description: 'Fetches paginated list of candidates (candidates) with total count',
  func: async ({ kysely }) => {
    const { count } = await kysely
      .selectFrom('candidate')
      .select(({ fn }) => fn.countAll().as('count'))
      .executeTakeFirstOrThrow()

    const candidates = await kysely
      .selectFrom('candidate')
      .selectAll()
      .orderBy('createdAt', 'desc')
      .execute()

    const data = candidates.map(omitLifecycleNotifications)

    return {
      total: Number(count),
      data: data.map((candidate) => ({
        ...candidate,
        stateWorkPreference: normalizeStringArray(candidate.stateWorkPreference),
        languagesSpoken: normalizeStringArray(candidate.languagesSpoken),
      })),
    }
  }
})

export const getBackOfficeCandidate = pikkuFunc<
  { candidateId: string },
  BackOfficeCandidate
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Get single candidate by ID',
  description: 'Fetches a specific candidate with all their structured document data',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId }) => {
    const candidate = await kysely
      .selectFrom('candidate')
      .selectAll()
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))

    // Fetch candidate documents once and group by category for verification.
    const candidateDocuments = await kysely
      .selectFrom('candidateDocument')
      .selectAll()
      .where('candidateId', '=', candidateId)
      .execute()

    const educationDocuments = candidateDocuments.filter((document) => document.category === CandidateDocumentTypes.EDUCATION)
    const licenseDocuments = candidateDocuments.filter((document) => document.category === CandidateDocumentTypes.LICENSE)
    const languageDocuments = candidateDocuments.filter((document) => document.category === CandidateDocumentTypes.LANGUAGE)
    const experienceDocuments = candidateDocuments.filter((document) => document.category === CandidateDocumentTypes.WORK_EXPERIENCE)

    // Fetch one analysis record per candidate/category.
    const education = await kysely
      .selectFrom('candidateEducation')
      .selectAll('candidateEducation')
      .where('candidateEducation.candidateId', '=', candidateId)
      .executeTakeFirst()

    const license = await kysely
      .selectFrom('candidateLicense')
      .selectAll('candidateLicense')
      .where('candidateLicense.candidateId', '=', candidateId)
      .executeTakeFirst()

    const language = await kysely
      .selectFrom('candidateLanguage')
      .selectAll('candidateLanguage')
      .where('candidateLanguage.candidateId', '=', candidateId)
      .executeTakeFirst()

    const experience = await kysely
      .selectFrom('candidateExperience')
      .selectAll('candidateExperience')
      .where('candidateExperience.candidateId', '=', candidateId)
      .orderBy('candidateExperience.createdAt', 'desc')
      .executeTakeFirst()

    const backOfficeCandidate = omitLifecycleNotifications(candidate)
    const lifecycleNotifications =
      (candidate as typeof candidate & {
        lifecycleNotifications?: CandidateLifecycleNotifications | null
      }).lifecycleNotifications ?? null

    return {
      ...backOfficeCandidate,
      stateWorkPreference: normalizeStringArray(backOfficeCandidate.stateWorkPreference),
      languagesSpoken: normalizeStringArray(backOfficeCandidate.languagesSpoken),
      lifecycleNotifications,
      analysis: {
        education: education || null,
        license: license || null,
        language: language || null,
        experience: experience || null,
      },
      documents: {
        education: educationDocuments,
        license: licenseDocuments,
        language: languageDocuments,
        experience: experienceDocuments,
      }
    }
  }
})

export const updateBackOfficeCandidate = pikkuFunc<
  { candidateId: string; analysisIssues?: string[] } & Partial<Pick<DB.Candidate, 'qualificationStatusHomeCountry' | 'qualificationStatusGermany' | 'licenseMandatory' | 'durationRequirementsMet' | 'status' | 'recognitionLikelihood' | 'dateOfBirth'>>,
  void
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate qualification status',
  description: 'Updates the qualification status fields for a candidate',
  errors: [NotFoundError],
  func: async ({ kysely, auth }, { candidateId, analysisIssues, ...updateData }, { http }) => {
    if (Object.keys(updateData).length === 0 && analysisIssues === undefined) {
      return
    }

    const currentCandidate = await kysely
      .selectFrom('candidate')
      .select(['candidateId', 'status', 'email', 'name', 'lifecycleNotifications'])
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))

    if (updateData.status && currentCandidate.status !== updateData.status && currentCandidate.email && currentCandidate.name) {
      // Backoffice-driven email notifications are intentionally limited to Complete only.
      // Invalid (and other lifecycle states) are handled by lifecycle rake tasks in BE.
      const shouldSendEmail =
        updateData.status === ApplicantStatuses.COMPLETE

      if (shouldSendEmail) {
        if (updateData.status === ApplicantStatuses.COMPLETE) {
          const authApi = await auth()
          await authApi.api.signInMagicLink({
            headers: forwardedAuthHeaders(http),
            body: {
              email: currentCandidate.email,
              name: currentCandidate.name,
              callbackURL: '/jobs/application',
              metadata: {
                template: 'candidate-qualified',
              },
            },
          })
        }
      }
    }

    if (Object.keys(updateData).length > 0) {
      await kysely
        .updateTable('candidate')
        .set(updateData)
        .where('candidateId', '=', candidateId)
        .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))
    }

    if (analysisIssues !== undefined) {
      const normalizedIssues = analysisIssues
        .map((issue) => issue?.trim())
        .filter((issue): issue is string => Boolean(issue))

      const lifecycleNotifications =
        (currentCandidate.lifecycleNotifications &&
        typeof currentCandidate.lifecycleNotifications === 'object' &&
        !Array.isArray(currentCandidate.lifecycleNotifications))
          ? currentCandidate.lifecycleNotifications
          : null

      const existingAnalysis =
        lifecycleNotifications?.analysis &&
        typeof lifecycleNotifications.analysis === 'object' &&
        !Array.isArray(lifecycleNotifications.analysis)
          ? lifecycleNotifications.analysis
          : null

      await kysely
        .updateTable('candidate')
        .set({
          lifecycleNotifications: {
            ...(lifecycleNotifications ?? {}),
            analysis: {
              ...(existingAnalysis ?? {}),
              issues: normalizedIssues,
              updated_at: new Date().toISOString(),
            },
          },
        })
        .where('candidateId', '=', candidateId)
        .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))
    }
  }
})

export const updateBackOfficeCandidateDocument = pikkuFunc<
  { candidateId: string; documentId: string; documentStatus: DB.DocumentStatus },
  { success: boolean }
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate document status',
  description: 'Updates the candidate document status',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId, documentId, documentStatus }) => {
    await kysely
      .updateTable('candidateDocument')
      .set({ documentStatus })
      .where('documentId', '=', documentId)
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Document not found'))

    return { success: true }
  }
})

export const updateBackOfficeCandidateEducation = pikkuFunc<
  {
    candidateId: string
  } & Partial<Omit<DB.CandidateEducation, 'candidateId' | 'educationId' | 'createdAt' | 'lastUpdatedAt'>>,
  void
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate education',
  description: 'Upserts the candidate education record',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId, ...updateData }) => {
    if (Object.keys(updateData).length === 0) {
      return
    }

    await kysely
      .insertInto('candidateEducation')
      .values({
        educationId: crypto.randomUUID(),
        candidateId,
        ...updateData
      })
      .onConflict((oc) => oc.column('candidateId').doUpdateSet(updateData))
      .execute()
  }
})

export const updateBackOfficeCandidateLicense = pikkuFunc<
  {
    candidateId: string
  } & Partial<Omit<DB.CandidateLicense, 'candidateId' | 'licenseId' | 'createdAt' | 'lastUpdatedAt'>>,
  void
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate license',
  description: 'Upserts the candidate license record',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId, ...updateData }) => {
    if (Object.keys(updateData).length === 0) {
      return
    }

    await kysely
      .insertInto('candidateLicense')
      .values({
        licenseId: crypto.randomUUID(),
        candidateId,
        ...updateData
      })
      .onConflict((oc) => oc.column('candidateId').doUpdateSet(updateData))
      .execute()
  }
})

export const updateBackOfficeCandidateLanguage = pikkuFunc<
  {
    candidateId: string
  } & Partial<Omit<DB.CandidateLanguage, 'candidateId' | 'languageId' | 'createdAt' | 'lastUpdatedAt'>>,
  void
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate language',
  description: 'Upserts the candidate language record',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId, ...updateData }) => {
    if (Object.keys(updateData).length === 0) {
      return
    }

    await kysely
      .insertInto('candidateLanguage')
      .values({
        languageId: crypto.randomUUID(),
        candidateId,
        ...updateData
      })
      .onConflict((oc) => oc.column('candidateId').doUpdateSet(updateData))
      .execute()
  }
})

export const updateBackOfficeCandidateExperience = pikkuFunc<
  {
    candidateId: string
  } & Partial<Omit<DB.CandidateExperience, 'candidateId' | 'experienceId' | 'createdAt' | 'lastUpdatedAt'>>,
  void
>({
  expose: true,
  permissions: { isBackOffice },
  title: 'Update candidate experience',
  description: 'Upserts the candidate experience record',
  errors: [NotFoundError],
  func: async ({ kysely }, { candidateId, ...updateData }) => {
    if (Object.keys(updateData).length === 0) {
      return
    }

    const existingExperience = await kysely
      .selectFrom('candidateExperience')
      .select('experienceId')
      .where('candidateId', '=', candidateId)
      .orderBy('createdAt', 'desc')
      .executeTakeFirst()

    if (existingExperience) {
      await kysely
        .updateTable('candidateExperience')
        .set({
          ...updateData,
          lastUpdatedAt: new Date(),
        })
        .where('experienceId', '=', existingExperience.experienceId)
        .execute()
      return
    }

    await kysely
      .insertInto('candidateExperience')
      .values({
        experienceId: crypto.randomUUID(),
        candidateId,
        ...updateData,
      })
      .execute()
  }
})
