import { and, eq } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import {
  isLocaleActive,
  markFailed,
  setTranslation,
  translationValue,
  type I18nContentSchema,
} from '@platform-modules/i18n-content'
import {
  executeWithFallback,
  getAdapter,
  type AIProvider,
  type FallbackChainEntry,
} from '@platform-modules/ai'
import { NoAdaptersConfiguredError, SourceUnavailableError, UnknownLocaleError } from './errors.js'
import { hashSource } from './hash.js'
import type {
  EntityTranslationResult,
  FieldOutcome,
  TranslateEntityArgs,
} from './model.js'

const PROVIDER_TRY_ORDER: readonly AIProvider[] = [
  'openai-compat',
  'anthropic',
  'google',
  'workers-ai',
]

const PROVIDER_DEFAULT_MODEL: Record<AIProvider, string> = {
  'openai-compat': 'gpt-4o-mini',
  anthropic: 'claude-haiku-4-5',
  google: 'gemini-1.5-flash',
  'workers-ai': '@cf/meta/llama-3.1-8b-instruct',
}

function resolveFallbackChain(modelId?: string): FallbackChainEntry[] {
  const resolvedModelId = modelId || undefined
  const chain: FallbackChainEntry[] = []
  for (const provider of PROVIDER_TRY_ORDER) {
    try {
      chain.push({
        adapter: getAdapter(provider, {}),
        model: PROVIDER_DEFAULT_MODEL[provider],
      })
    } catch {
      continue
    }
  }
  if (chain.length === 0) {
    throw new NoAdaptersConfiguredError()
  }
  // Apply caller's modelId to the primary entry only; fallback entries keep
  // their own provider defaults so a foreign model id is never sent cross-provider.
  if (resolvedModelId) chain[0]!.model = resolvedModelId
  return chain
}

function buildTranslationPrompt(
  sourceLocale: string,
  targetLocale: string,
  fields: ReadonlyArray<{ fieldKey: string; value: string }>,
): string {
  const fieldLines = fields.map((f) => `- ${f.fieldKey}: ${f.value}`).join('\n')
  return [
    `Translate the following fields from ${sourceLocale} to ${targetLocale}.`,
    'Return ONLY valid JSON with the same field keys and translated string values.',
    'Fields:',
    fieldLines,
  ].join('\n')
}

/**
 * Strip markdown code fences and/or prose preamble before JSON.parse.
 * Models (gpt-4o-mini, gemini-1.5-flash, llama) routinely emit ```json
 * wrappers or leading prose — raw JSON.parse throws on those, marking the
 * entire field-batch FAILED. This extracts the JSON object robustly.
 */
function extractJson(content: string): string {
  // Strip ```json ... ``` or ``` ... ``` fences
  const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/)
  if (fenced) return fenced[1]!.trim()
  // Fallback: extract from first { to last }
  const start = content.indexOf('{')
  const end = content.lastIndexOf('}')
  if (start !== -1 && end !== -1 && end > start) return content.slice(start, end + 1)
  return content.trim()
}

/** Maximum allowed length (chars) for a single translated field value. */
const MAX_TRANSLATION_FIELD_LENGTH = 50_000

function parseTranslationResponse(
  content: string,
  fieldKeys: readonly string[],
): Record<string, string> {
  const parsed = JSON.parse(extractJson(content)) as unknown
  if (typeof parsed !== 'object' || parsed === null) {
    throw new Error('translation response is not a JSON object')
  }
  const record = parsed as Record<string, unknown>
  const out: Record<string, string> = {}
  for (const fieldKey of fieldKeys) {
    const value = record[fieldKey]
    if (typeof value !== 'string') {
      throw new Error(`missing or invalid translation for field '${fieldKey}'`)
    }
    if (value.length === 0) {
      throw new Error(`empty translation returned for field '${fieldKey}'`)
    }
    if (value.length > MAX_TRANSLATION_FIELD_LENGTH) {
      throw new Error(
        `translation for field '${fieldKey}' exceeds maximum length (${value.length} chars)`,
      )
    }
    out[fieldKey] = value
  }
  return out
}

export async function translateEntity(
  q: Querier<I18nContentSchema>,
  args: TranslateEntityArgs,
): Promise<EntityTranslationResult> {
  for (const locale of args.targetLocales) {
    if (!(await isLocaleActive(q, locale))) {
      throw new UnknownLocaleError(locale)
    }
  }

  let sourceLocale: string
  let sourceFields: Record<string, string>
  try {
    const sourceResult = await args.source.getSourceText({
      entityType: args.entityType,
      entityId: args.entityId,
      fields: args.fields,
    })
    sourceLocale = sourceResult.locale
    sourceFields = sourceResult.fields
  } catch {
    throw new SourceUnavailableError(args.entityType, args.entityId)
  }

  const fieldHashes = new Map<string, string>()
  for (const fieldKey of args.fields) {
    if (fieldKey in sourceFields) {
      fieldHashes.set(fieldKey, await hashSource(sourceFields[fieldKey]!))
    }
  }

  const skipUnchanged = args.skipUnchanged !== false
  const normalizedModelId = args.modelId || null // '' treated same as omitted
  const results: EntityTranslationResult['results'] = []

  for (const targetLocale of args.targetLocales) {
    const fieldOutcomes: FieldOutcome[] = []
    const toTranslate: Array<{ fieldKey: string; value: string; sourceHash: string }> = []

    if (targetLocale === sourceLocale) {
      for (const fieldKey of args.fields) {
        fieldOutcomes.push({ fieldKey, outcome: 'SKIPPED' })
      }
      results.push({ locale: targetLocale, fields: fieldOutcomes })
      continue
    }

    for (const fieldKey of args.fields) {
      if (!(fieldKey in sourceFields)) {
        fieldOutcomes.push({ fieldKey, outcome: 'SKIPPED' })
        continue
      }

      const sourceHash = fieldHashes.get(fieldKey)!
      const sourceValue = sourceFields[fieldKey]!

      if (sourceValue === '') {
        fieldOutcomes.push({ fieldKey, outcome: 'SKIPPED' })
        continue
      }

      if (skipUnchanged) {
        // NOTE: the hash-check → translate → write sequence is not atomic.
        // Concurrent invocations may both pass this check and both issue an AI call,
        // resulting in double AI spend but a correct final state (both writes
        // converge to the same value). A proper atomic fix would require a
        // conditional upsert at the setTranslation seam; deferred as known limitation.
        const [existing] = await q
          .select({
            sourceHash: translationValue.sourceHash,
            status: translationValue.status,
          })
          .from(translationValue)
          .where(
            and(
              eq(translationValue.entityType, args.entityType),
              eq(translationValue.entityId, args.entityId),
              eq(translationValue.fieldKey, fieldKey),
              eq(translationValue.locale, targetLocale),
            ),
          )
          .limit(1)

        if (existing?.sourceHash === sourceHash && existing.status === 'OK') {
          fieldOutcomes.push({ fieldKey, outcome: 'SKIPPED' })
          continue
        }
      }

      toTranslate.push({ fieldKey, value: sourceValue, sourceHash })
    }

    if (toTranslate.length === 0) {
      results.push({ locale: targetLocale, fields: fieldOutcomes })
      continue
    }

    const chain = resolveFallbackChain(args.modelId)
    const reqModel = (args.modelId || undefined) ?? chain[0]!.model!
    const prompt = buildTranslationPrompt(
      sourceLocale,
      targetLocale,
      toTranslate.map((f) => ({ fieldKey: f.fieldKey, value: f.value })),
    )

    let translated: Record<string, string>
    try {
      const response = await executeWithFallback(
        {
          model: reqModel,
          messages: [{ role: 'user', content: prompt }],
          maxTokens: 8192,
          temperature: 0,
        },
        [...chain],
        // A non-retryable error from one provider (e.g. AuthError for bad credentials)
        // should not abort the chain — other providers may still be healthy.
        { continueOnNonRetryable: true },
      )
      translated = parseTranslationResponse(
        response.content,
        toTranslate.map((f) => f.fieldKey),
      )
    } catch {
      for (const item of toTranslate) {
        await markFailed(q, {
          entityType: args.entityType,
          entityId: args.entityId,
          fieldKey: item.fieldKey,
          locale: targetLocale,
          sourceHash: item.sourceHash,
          modelId: normalizedModelId,
        })
        fieldOutcomes.push({ fieldKey: item.fieldKey, outcome: 'FAILED' })
      }
      results.push({ locale: targetLocale, fields: fieldOutcomes })
      continue
    }

    for (const item of toTranslate) {
      try {
        await setTranslation(q, {
          entityType: args.entityType,
          entityId: args.entityId,
          fieldKey: item.fieldKey,
          locale: targetLocale,
          value: translated[item.fieldKey]!,
          sourceHash: item.sourceHash,
          modelId: normalizedModelId,
          manualOverride: false,
        })
        fieldOutcomes.push({ fieldKey: item.fieldKey, outcome: 'OK' })
      } catch {
        await markFailed(q, {
          entityType: args.entityType,
          entityId: args.entityId,
          fieldKey: item.fieldKey,
          locale: targetLocale,
          sourceHash: item.sourceHash,
          modelId: normalizedModelId,
        })
        fieldOutcomes.push({ fieldKey: item.fieldKey, outcome: 'FAILED' })
      }
    }

    results.push({ locale: targetLocale, fields: fieldOutcomes })
  }

  return { results }
}
