import type {
  Db,
  InvoiceGenerateJob,
  JobIdempotencyKeyRow,
  PlatformJobRow,
  RetainerInvoiceJob,
} from '@zync/db/queries'
import {
  claimJobIdempotencyKey,
  claimPendingPlatformJobs as claimPendingPlatformJobsQuery,
  listJobIdempotencyKeys,
  listPlatformJobs as listPlatformJobsQuery,
  markJobIdempotencyKeyProcessed,
  markPlatformJobProcessed as markPlatformJobProcessedQuery,
  requeuePlatformJob as requeuePlatformJobQuery,
  releaseJobIdempotencyKey,
} from '@zync/db/queries'
import type { MessageBatch, Message } from '@cloudflare/workers-types'
import {
  createJobRegistry,
  type DispatchOpts,
  type IdempotencyStore,
  type JobRegistry,
  TerminalJobError,
} from '@platform-modules/jobs'
import type { Env } from '@zync/types'
import type { LeadScoreRecalcJob } from '../../queues/lead-score-recalc'
import type { UniformExportJob } from '../../reports/uniform-format/types'

export type ReportScheduleJob = {
  type: 'report.schedule'
  scheduleId: string
  tenantId: string
  oneOff?: boolean
}

export type PlatformQueueJob =
  | InvoiceGenerateJob
  | LeadScoreRecalcJob
  | ReportScheduleJob
  | RetainerInvoiceJob
  | UniformExportJob

export type PlatformJobsEnv = Env

export type PlatformJobsHandlers = {
  handleInvoiceGenerate: (env: PlatformJobsEnv, job: InvoiceGenerateJob) => Promise<void>
  handleLeadScoreRecalc: (env: PlatformJobsEnv, job: LeadScoreRecalcJob) => Promise<void>
  handleReportSchedule: (env: PlatformJobsEnv, job: ReportScheduleJob) => Promise<void>
  handleRetainerInvoice: (env: PlatformJobsEnv, job: RetainerInvoiceJob) => Promise<void>
  handleUniformExport: (env: PlatformJobsEnv, job: UniformExportJob) => Promise<void>
}

type PlatformSchema<T> = {
  '~standard': {
    version: 1
    vendor: 'platform-modules-migration'
    validate(value: unknown): { value: T } | { issues: Array<{ message: string }> }
  }
}

export type HostInjectedIdempotencyStore = IdempotencyStore & {
  claim(key: string, payload?: unknown): Promise<boolean>
  markProcessed(key: string, ttlSeconds?: number): Promise<void>
  release(key: string): Promise<void>
  list(): Promise<JobIdempotencyKeyRow[]>
}

function schemaFromGuard<T>(guard: (value: unknown) => value is T): PlatformSchema<T> {
  return {
    '~standard': {
      version: 1,
      vendor: 'platform-modules-migration',
      validate(value) {
        return guard(value) ? { value } : { issues: [{ message: 'invalid job payload' }] }
      },
    },
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}

function hasString(value: Record<string, unknown>, key: string): boolean {
  return typeof value[key] === 'string' && value[key]!.length > 0
}

function isUniformExportJob(value: unknown): value is UniformExportJob {
  return isRecord(value)
    && value.type === 'uniform-format'
    && hasString(value, 'jobId')
    && hasString(value, 'tenantId')
    && hasString(value, 'from')
    && hasString(value, 'to')
    && hasString(value, 'mode')
    && hasString(value, 'userId')
}

function isLeadScoreRecalcJob(value: unknown): value is LeadScoreRecalcJob {
  return isRecord(value)
    && value.type === 'lead.score_recalc'
    && hasString(value, 'tenantId')
    && hasString(value, 'leadId')
}

function isReportScheduleJob(value: unknown): value is ReportScheduleJob {
  return isRecord(value)
    && value.type === 'report.schedule'
    && hasString(value, 'scheduleId')
    && hasString(value, 'tenantId')
    && (value.oneOff === undefined || typeof value.oneOff === 'boolean')
}

function isRetainerInvoiceJob(value: unknown): value is RetainerInvoiceJob {
  return isRecord(value)
    && value.type === 'retainer.invoice'
    && hasString(value, 'tenant_id')
    && hasString(value, 'project_id')
    && hasString(value, 'month')
}

function isInvoiceGenerateJob(value: unknown): value is InvoiceGenerateJob {
  return isRecord(value)
    && value.type === 'invoice.generate'
    && hasString(value, 'tenantId')
    && hasString(value, 'projectId')
    && hasString(value, 'reason')
}

export function idempotencyKeyForJob(job: PlatformQueueJob): string {
  switch (job.type) {
    case 'uniform-format':
      return `uniform-format:${job.jobId}`
    case 'lead.score_recalc':
      return `lead.score_recalc:${job.tenantId}:${job.leadId}`
    case 'report.schedule':
      return `report.schedule:${job.tenantId}:${job.scheduleId}:${job.oneOff === true ? 'one-off' : 'scheduled'}`
    case 'retainer.invoice':
      return `retainer.invoice:${job.tenant_id}:${job.project_id}:${job.month}`
    case 'invoice.generate':
      return `invoice.generate:${job.tenantId}:${job.projectId}:${job.reason}:${job.month ?? 'none'}`
  }
}

export function createPlatformJobsRegistry(
  handlers: PlatformJobsHandlers,
): JobRegistry<PlatformJobsEnv> {
  const registry = createJobRegistry<PlatformJobsEnv>()
  registry.register('uniform-format', schemaFromGuard(isUniformExportJob), async (env, payload) => {
    await handlers.handleUniformExport(env, payload as UniformExportJob)
  })
  registry.register('lead.score_recalc', schemaFromGuard(isLeadScoreRecalcJob), async (env, payload) => {
    await handlers.handleLeadScoreRecalc(env, payload as LeadScoreRecalcJob)
  })
  registry.register('report.schedule', schemaFromGuard(isReportScheduleJob), async (env, payload) => {
    await handlers.handleReportSchedule(env, payload as ReportScheduleJob)
  })
  registry.register('retainer.invoice', schemaFromGuard(isRetainerInvoiceJob), async (env, payload) => {
    await handlers.handleRetainerInvoice(env, payload as RetainerInvoiceJob)
  })
  registry.register('invoice.generate', schemaFromGuard(isInvoiceGenerateJob), async (env, payload) => {
    await handlers.handleInvoiceGenerate(env, payload as InvoiceGenerateJob)
  })
  return registry
}

export function createHostInjectedIdempotencyStore(
  db: Db,
  now: () => Date = () => new Date(),
): HostInjectedIdempotencyStore {
  return {
    async claim(key, payload) {
      return claimJobIdempotencyKey(db, key, now(), payload)
    },
    async seen(key) {
      return !(await this.claim(key))
    },
    async mark(key, ttlSeconds) {
      await this.markProcessed(key, ttlSeconds)
    },
    async markProcessed(key, ttlSeconds) {
      const current = now()
      const expiresAt = ttlSeconds && ttlSeconds > 0
        ? new Date(current.getTime() + ttlSeconds * 1000)
        : null
      await markJobIdempotencyKeyProcessed(db, key, current, expiresAt)
    },
    async release(key) {
      await releaseJobIdempotencyKey(db, key)
    },
    async list() {
      return listJobIdempotencyKeys(db, now())
    },
  }
}

export async function consumePlatformJobsBatch(
  batch: MessageBatch<PlatformQueueJob>,
  env: PlatformJobsEnv,
  registry: JobRegistry<PlatformJobsEnv>,
  store: HostInjectedIdempotencyStore,
  dispatchOpts?: Omit<DispatchOpts<PlatformJobsEnv>, 'idempotency'>,
): Promise<void> {
  for (const msg of batch.messages as Array<Message<PlatformQueueJob>>) {
    const key = idempotencyKeyForJob(msg.body)
    try {
      await registry.dispatch(env, msg.body as never, { ...dispatchOpts, idempotency: { key, store } })
      msg.ack()
    } catch (error) {
      await store.release(key)
      if (error instanceof TerminalJobError) {
        msg.ack()
        continue
      }
      msg.retry()
    }
  }
}

export async function claimPendingPlatformJobs(db: Db, limit: number): Promise<PlatformJobRow[]> {
  return claimPendingPlatformJobsQuery(db, limit)
}

export async function markPlatformJobProcessed(db: Db, jobId: string): Promise<void> {
  await markPlatformJobProcessedQuery(db, jobId)
}

export async function requeuePlatformJob(
  db: Db,
  jobId: string,
  error: string,
): Promise<PlatformJobRow | undefined> {
  return requeuePlatformJobQuery(db, jobId, error)
}

export async function listPlatformJobs(db: Db, jobIds?: readonly string[]): Promise<PlatformJobRow[]> {
  return listPlatformJobsQuery(db, jobIds)
}

type BackfillImportJobRow = {
  id: string
  tenantId: string
  status: string
  errorMessage: string | null
  createdAt: Date
  startedAt: Date | null
  completedAt: Date | null
  columnMapping: Record<string, unknown> | null
}

type BackfillUniformExportJobRow = {
  id: string
  tenantId: string
  status: string
  errorMessage: string | null
  createdAt: Date
}

type BackfillTenantExportJobRow = {
  id: string
  tenantId: string
  status: string
  createdAt: Date
  completedAt: Date | null
}

function mapLegacyStatus(status: string): PlatformJobRow['status'] {
  switch (status.toLowerCase()) {
    case 'processing':
    case 'running':
      return 'processing'
    case 'completed':
    case 'done':
      return 'completed'
    case 'failed':
    case 'error':
      return 'failed'
    default:
      return 'pending'
  }
}

export function buildBackfillJobRows(input: {
  invoiceGenerationJobs: BackfillImportJobRow[]
  tenantExportJobs: BackfillTenantExportJobRow[]
  uniformExportJobs: BackfillUniformExportJobRow[]
}): Array<PlatformJobRow> {
  return [
    ...input.invoiceGenerationJobs.map((job) => ({
      id: job.id,
      type: 'invoice.generate',
      payload: { tenantId: job.tenantId, ...(job.columnMapping ?? {}) },
      status: mapLegacyStatus(job.status),
      attempts: 0,
      maxAttempts: 5,
      lastError: job.errorMessage,
      createdAt: job.createdAt,
      scheduledFor: job.startedAt ?? job.createdAt,
      processedAt: job.completedAt,
      failedAt: mapLegacyStatus(job.status) === 'failed' ? job.completedAt ?? job.createdAt : null,
    })),
    ...input.uniformExportJobs.map((job) => ({
      id: job.id,
      type: 'uniform-format',
      payload: { tenantId: job.tenantId, jobId: job.id },
      status: mapLegacyStatus(job.status),
      attempts: 0,
      maxAttempts: 5,
      lastError: job.errorMessage,
      createdAt: job.createdAt,
      scheduledFor: job.createdAt,
      processedAt: mapLegacyStatus(job.status) === 'completed' ? job.createdAt : null,
      failedAt: mapLegacyStatus(job.status) === 'failed' ? job.createdAt : null,
    })),
    ...input.tenantExportJobs.map((job) => ({
      id: job.id,
      type: 'tenant.export',
      payload: { tenantId: job.tenantId, jobId: job.id },
      status: mapLegacyStatus(job.status),
      attempts: 0,
      maxAttempts: 5,
      lastError: null,
      createdAt: job.createdAt,
      scheduledFor: job.createdAt,
      processedAt: mapLegacyStatus(job.status) === 'completed' ? job.completedAt : null,
      failedAt: mapLegacyStatus(job.status) === 'failed' ? job.completedAt ?? job.createdAt : null,
    })),
  ]
}
