import { pikkuFunc } from '#pikku/pikku-types.gen.js'
import { NotFoundError } from '@pikku/core/errors'
import type {
  DB,
  GetCompanyCandidatesOutput,
  PublicCandidateInfo,
} from '@heygermany/sdk'
import {
  ApplicantStatuses,
  CandidateDocumentTypes,
  DocumentStatuses,
} from '@heygermany/sdk'
import dayjs from 'dayjs'
import { Kysely, sql } from 'kysely'
import type { KyselyDB } from '../../database-types.js'
import { isBackOffice, isCompany } from '../../permissions.js'
import { toBucketKey } from '../../content-path.js'
import { normalizeStringArray } from '../../normalizers.js'

const PROFILE_IMAGE_CATEGORY = 'profile_image'

type ProfileImageSigningServices = {
  content: {
    signContentKey: (args: {
      bucket: string
      contentKey: string
      dateLessThan: Date
    }) => Promise<string>
  }
  kysely: Kysely<KyselyDB>
  logger: {
    warn: (...args: any[]) => void
  }
}

// Helper function to calculate age from date of birth
const calculateAge = (dateOfBirth: Date | null): number | null => {
  if (!dateOfBirth) return null
  const today = new Date()
  const birthDate = new Date(dateOfBirth)
  let age = today.getFullYear() - birthDate.getFullYear()
  const monthDiff = today.getMonth() - birthDate.getMonth()
  if (
    monthDiff < 0 ||
    (monthDiff === 0 && today.getDate() < birthDate.getDate())
  ) {
    age--
  }
  return age
}

const getSignedProfileImageUrls = async (
  { kysely, content, logger }: ProfileImageSigningServices,
  candidateIds: string[]
) => {
  if (candidateIds.length === 0) {
    return new Map<string, string>()
  }

  const profileImages = await kysely
    .selectFrom('candidateDocument')
    .select(['candidateId', 'documentId', 'filePath'])
    .where('candidateId', 'in', candidateIds)
    .where('category', '=', PROFILE_IMAGE_CATEGORY as any)
    .where('uploaded', '=', true)
    .orderBy('lastUpdatedAt', 'desc')
    .execute()

  const urlEntries = await Promise.all(
    profileImages.map(async (document) => {
      try {
        const { bucket, key } = toBucketKey(document.filePath)
        return [
          document.candidateId,
          await content.signContentKey({
            bucket,
            contentKey: key,
            dateLessThan: dayjs().add(15, 'minutes').toDate(),
          }),
        ] as const
      } catch (error) {
        logger.warn(
          {
            candidateId: document.candidateId,
            documentId: document.documentId,
            error,
          },
          'Failed to sign candidate profile image'
        )
        return null
      }
    })
  )

  const profileImageUrls = new Map<string, string>()
  for (const entry of urlEntries) {
    if (!entry) continue
    const [candidateId, url] = entry
    if (!profileImageUrls.has(candidateId)) {
      profileImageUrls.set(candidateId, url)
    }
  }

  return profileImageUrls
}

const getExperienceMonthIndex = (
  date: Date | string | null | undefined
): number | null => {
  if (!date) return null

  const parsedDate = new Date(date)
  if (Number.isNaN(parsedDate.getTime())) return null

  return parsedDate.getUTCFullYear() * 12 + parsedDate.getUTCMonth()
}

const calculateTotalExperienceMonths = (
  experienceEntries: Array<{
    startDate: Date | string | null
    endDate: Date | string | null
  }>
) => {
  const normalizedRanges = experienceEntries
    .map(({ startDate, endDate }) => {
      const startMonth = getExperienceMonthIndex(startDate)
      const endMonth = getExperienceMonthIndex(endDate || new Date())

      if (
        startMonth === null ||
        endMonth === null ||
        endMonth < startMonth
      ) {
        return null
      }

      return { startMonth, endMonth }
    })
    .filter((range): range is { startMonth: number; endMonth: number } =>
      Boolean(range)
    )
    .sort((a, b) => a.startMonth - b.startMonth)

  if (normalizedRanges.length === 0) {
    return null
  }

  const mergedRanges = [normalizedRanges[0]]
  for (const range of normalizedRanges.slice(1)) {
    const lastRange = mergedRanges[mergedRanges.length - 1]

    if (range.startMonth <= lastRange.endMonth + 1) {
      lastRange.endMonth = Math.max(lastRange.endMonth, range.endMonth)
      continue
    }

    mergedRanges.push({ ...range })
  }

  return mergedRanges.reduce(
    (total, range) => total + (range.endMonth - range.startMonth + 1),
    0
  )
}

const getPublicLanguageFields = ({
  languageAnalysis,
  languageCertificateProvided,
  languageSelfAssessmentLevel,
}: {
  languageAnalysis?: {
    level: DB.LanguageLevel | null
  } | null
  languageCertificateProvided: boolean | null
  languageSelfAssessmentLevel: DB.LanguageLevel | null
}) => ({
  languageCertificateProvided:
    languageAnalysis ? true : languageCertificateProvided,
  languageSelfAssessmentLevel:
    languageAnalysis?.level ?? languageSelfAssessmentLevel,
})

export const getCompanyCandidates = pikkuFunc<
  { offset?: number; limit?: number },
  GetCompanyCandidatesOutput
>({
  expose: true,
  title: 'Get all candidates (public info)',
  description:
    'Fetches paginated list of candidates with non-personal information only',
  permissions: {
    isBackOffice,
    isCompany,
  },
  func: async ({ kysely, content, logger }) => {
    const { count } = await kysely
      .selectFrom('candidate')
      .select(({ fn }) => fn.countAll().as('count'))
      .executeTakeFirstOrThrow()

    const candidates = await kysely
      .selectFrom('candidate')
      .select([
        'candidateId',
        'name',
        'submittedAt',
        'stateWorkPreference',
        'joinTalentPool',
        'livingInGermany',
        'euPassport',
        'countryEducation',
        'languageCertificateProvided',
        'languageSelfAssessmentLevel',
        'qualificationStatusGermany',
        'qualificationStatusHomeCountry',
        'currentResidence',
        'createdAt',
        'languagesSpoken',
        'nationality',
        'professionalRecognitionStatus',
        'recognitionLikelihood',
        'dateOfBirth',
      ])
      .where('candidate.status', '=', ApplicantStatuses.COMPLETE)
      .orderBy('createdAt', 'desc')
      .execute()

    const candidateIds = candidates.map(({ candidateId }) => candidateId)

    const educationAnalyses =
      candidateIds.length > 0
        ? await kysely
            .selectFrom('candidateEducation')
            .select([
              'candidateId',
              'institution',
              'program',
              'country',
              'completionDate',
              'nursingRelatedDegree',
            ])
            .where('candidateId', 'in', candidateIds)
            .execute()
        : []

    const educationByCandidateId = new Map(
      educationAnalyses.map(({ candidateId, ...education }) => [
        candidateId,
        education,
      ])
    )

    const languageAnalyses =
      candidateIds.length > 0
        ? await kysely
            .selectFrom('candidateLanguage')
            .select(['candidateId', 'level'])
            .where('candidateId', 'in', candidateIds)
            .execute()
        : []

    const languageByCandidateId = new Map(
      languageAnalyses.map(({ candidateId, ...languageAnalysis }) => [
        candidateId,
        languageAnalysis,
      ])
    )

    const experienceAnalyses =
      candidateIds.length > 0
        ? await kysely
            .selectFrom('candidateExperience')
            .select(['candidateId', 'startDate', 'endDate'])
            .where('candidateId', 'in', candidateIds)
            .execute()
        : []

    const experienceByCandidateId = new Map<
      string,
      Array<{
        startDate: Date | null
        endDate: Date | null
      }>
    >()

    for (const experience of experienceAnalyses) {
      const candidateExperiences =
        experienceByCandidateId.get(experience.candidateId) || []
      candidateExperiences.push({
        startDate: experience.startDate,
        endDate: experience.endDate,
      })
      experienceByCandidateId.set(experience.candidateId, candidateExperiences)
    }

    const profileImageUrls = await getSignedProfileImageUrls(
      { kysely, content, logger },
      candidateIds
    )

    const data: PublicCandidateInfo[] = candidates.map(
      ({ dateOfBirth, ...candidate }) => {
        const publicLanguageFields = getPublicLanguageFields({
          languageAnalysis: languageByCandidateId.get(candidate.candidateId),
          languageCertificateProvided: candidate.languageCertificateProvided,
          languageSelfAssessmentLevel: candidate.languageSelfAssessmentLevel,
        })

        return {
          ...candidate,
          stateWorkPreference: normalizeStringArray(candidate.stateWorkPreference),
          languagesSpoken: normalizeStringArray(candidate.languagesSpoken),
          ...publicLanguageFields,
          age: calculateAge(dateOfBirth),
          hasVerifiedLicense: false,
          profileImageUrl: profileImageUrls.get(candidate.candidateId) || null,
          totalExperienceMonths: calculateTotalExperienceMonths(
            experienceByCandidateId.get(candidate.candidateId) || []
          ),
          experience: [],
          education: educationByCandidateId.get(candidate.candidateId) || null,
        }
      }
    )

    return {
      total: Number(count),
      data,
    }
  },
})

export const getCompanyCandidate = pikkuFunc<
  { candidateId: string },
  PublicCandidateInfo
>({
  expose: true,
  title: 'Get single candidate by ID (public info)',
  description:
    'Fetches a specific candidate with non-personal information only',
  errors: [NotFoundError],
  permissions: {
    isBackOffice,
    isCompany,
  },
  func: async ({ kysely, content, logger }, { candidateId }) => {
    const { dateOfBirth, ...candidate } = await kysely
      .selectFrom('candidate')
      .select([
        'candidateId',
        'name',
        'submittedAt',
        'stateWorkPreference',
        'joinTalentPool',
        'livingInGermany',
        'euPassport',
        'countryEducation',
        'languageCertificateProvided',
        'languageSelfAssessmentLevel',
        'qualificationStatusGermany',
        'qualificationStatusHomeCountry',
        'currentResidence',
        'createdAt',
        'languagesSpoken',
        'nationality',
        'professionalRecognitionStatus',
        'recognitionLikelihood',
        'dateOfBirth',
      ])
      .where('candidateId', '=', candidateId)
      .executeTakeFirstOrThrow(() => new NotFoundError('Candidate not found'))

    // Check if candidate has a verified license document.
    const verifiedLicenseDocument = await kysely
      .selectFrom('candidateDocument')
      .select('documentId')
      .where('candidateId', '=', candidateId)
      .where('category', '=', CandidateDocumentTypes.LICENSE)
      .where('documentStatus', '=', DocumentStatuses.VERIFIED)
      .executeTakeFirst()

    const educationAnalysis = await kysely
      .selectFrom('candidateEducation')
      .select([
        'institution',
        'program',
        'country',
        'completionDate',
        'nursingRelatedDegree',
      ])
      .where('candidateEducation.candidateId', '=', candidateId)
      .executeTakeFirst()

    const languageAnalysis = await kysely
      .selectFrom('candidateLanguage')
      .select(['level'])
      .where('candidateLanguage.candidateId', '=', candidateId)
      .executeTakeFirst()

    const experienceAnalysis = await kysely
      .selectFrom('candidateExperience')
      .select([
        sql<string | null>`${sql.ref(
          'candidate_experience.employer_name'
        )}`.as(
          'employerName'
        ),
        sql<string | null>`${sql.ref('candidate_experience.role_title')}`.as(
          'roleTitle'
        ),
        'candidateExperience.department',
        sql<string | null>`${sql.ref('candidate_experience.country')}`.as(
          'country'
        ),
        sql<string | null>`${sql.ref('candidate_experience.city')}`.as('city'),
        sql<string | null>`${sql.ref(
          'candidate_experience.facility_type'
        )}`.as(
          'facilityType'
        ),
        'candidateExperience.startDate',
        'candidateExperience.endDate',
      ])
      .where('candidateExperience.candidateId', '=', candidateId)
      .orderBy('candidateExperience.startDate', 'desc')
      .orderBy('candidateExperience.createdAt', 'desc')
      .execute()

    const profileImageUrls = await getSignedProfileImageUrls(
      { kysely, content, logger },
      [candidateId]
    )

    const publicLanguageFields = getPublicLanguageFields({
      languageAnalysis,
      languageCertificateProvided: candidate.languageCertificateProvided,
      languageSelfAssessmentLevel: candidate.languageSelfAssessmentLevel,
    })

    return {
      ...candidate,
      stateWorkPreference: normalizeStringArray(candidate.stateWorkPreference),
      languagesSpoken: normalizeStringArray(candidate.languagesSpoken),
      ...publicLanguageFields,
      age: calculateAge(dateOfBirth),
      hasVerifiedLicense: !!verifiedLicenseDocument,
      profileImageUrl: profileImageUrls.get(candidateId) || null,
      totalExperienceMonths: calculateTotalExperienceMonths(experienceAnalysis),
      experience: experienceAnalysis.map((experience) => ({
        employerName: experience.employerName,
        roleTitle: experience.roleTitle,
        department: experience.department,
        country: experience.country,
        city: experience.city,
        facilityType: experience.facilityType,
        startDate: experience.startDate,
        endDate: experience.endDate,
      })),
      education: educationAnalysis || null,
    }
  },
})
