import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
import {
  Alert,
  Button,
  FormActions,
  Modal,
  MultiSelect,
  SingleSelect,
  Tabs,
  TextArea,
  TextInput,
  type SelectOption,
  type TabItem,
} from '@/ui'
import { useCreateDictionary, useUpdateDictionary } from '@/api/mutations'
import { dictionaryDetailQuery, useDictionaries } from '@/api/queries'
import type { CreateDictionaryRequest, DataScope, DictionaryDetail } from '@/api/client'
import { SchemaBuilder } from './SchemaBuilder'
import { EventsPreviewTab } from './EventsPreviewTab'
import { CreateSchemaDraftModal } from '@/components/workflow/CreateSchemaDraftModal'
import {
  buildSchemaJson,
  diffSchemas,
  hasBreakingChanges,
  parseSchemaJson,
  suggestVersionBump,
  type PropertyDef,
  type SchemaChange,
} from './types'

type Mode =
  | { kind: 'create' }
  | { kind: 'edit'; existing: DictionaryDetail }

type Props = {
  open: boolean
  mode: Mode
  onClose: () => void
  onSuccess: (name: string) => void
}

const DEFAULT_LOCALES = ['ru-RU', 'en-US', 'zh-CN']

const SCOPE_OPTIONS = (t: (k: string) => string): SelectOption[] => [
  { id: 'PUBLIC', label: t('scope.PUBLIC') },
  { id: 'INTERNAL', label: t('scope.INTERNAL') },
  { id: 'RESTRICTED', label: t('scope.RESTRICTED') },
]

type EditorTab = 'metadata' | 'schema' | 'preview' | 'events'

/**
 * Decide whether the editor's Save button should be blocked because the
 * change has to go through the schema-draft workflow instead of inline PUT.
 * Mirrors the three backend gate clauses in DictionaryDefinitionService
 * exactly so the UX matches the server contract.
 *
 * Returns false (don't block) for create-mode, for non-approval-required
 * dicts, and for pure metadata edits (display name / description / locales
 * etc.) that leave schema + approval config alone.
 *
 * Exported for unit testability — the three-clause logic is the load-bearing
 * piece, easier to verify in isolation than mounting the full editor with
 * its query and mutation deps.
 */
export function computeRequiresDraftFlow({
  isEdit,
  initialApprovalRequired,
  initialMinRole,
  currentApprovalRequired,
  currentMinRole,
  schemaChangeCount,
}: {
  isEdit: boolean
  initialApprovalRequired: boolean
  initialMinRole: string | null
  currentApprovalRequired: boolean
  currentMinRole: string | null
  schemaChangeCount: number
}): boolean {
  if (!isEdit || !initialApprovalRequired) return false
  const schemaChanged = schemaChangeCount > 0
  const approvalToggledOff = !currentApprovalRequired
  const minRoleChanged = initialMinRole !== currentMinRole
  return schemaChanged || approvalToggledOff || minRoleChanged
}

export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props) => {
  const { t } = useTranslation()
  const createMut = useCreateDictionary()
  const updateMut = useUpdateDictionary()

  const isEdit = mode.kind === 'edit'
  const initial = mode.kind === 'edit' ? mode.existing : null

  const parsed = useMemo(() => parseSchemaJson(initial?.schemaJson), [initial])

  const [activeTab, setActiveTab] = useState<EditorTab>('metadata')
  const [name, setName] = useState(initial?.name ?? '')
  const [displayName, setDisplayName] = useState(initial?.displayName ?? '')
  const [description, setDescription] = useState(initial?.description ?? '')
  const [scope, setScope] = useState<DataScope>(initial?.scope ?? 'PUBLIC')
  const [bundle, setBundle] = useState(initial?.bundle ?? 'cuod')
  const [schemaVersion, setSchemaVersion] = useState(initial?.schemaVersion ?? '1.0.0')
  const [supportedLocales, setSupportedLocales] = useState<string[]>(
    initial?.supportedLocales ?? ['ru-RU'],
  )
  const [defaultLocale, setDefaultLocale] = useState(initial?.defaultLocale ?? 'ru-RU')
  const [redisProjection, setRedisProjection] = useState(
    initial?.redisProjectionEnabled ?? false,
  )
  const [approvalRequired, setApprovalRequired] = useState(
    (initial as { approvalRequired?: boolean } | null)?.approvalRequired ?? false,
  )
  const [approvalMinRole, setApprovalMinRole] = useState(
    (initial as { approvalMinRole?: string } | null)?.approvalMinRole ?? '',
  )
  const [properties, setProperties] = useState<PropertyDef[]>(parsed.properties)
  const [idSource, setIdSource] = useState<string>(parsed.idSource ?? '')
  const [nameError, setNameError] = useState<string | null>(null)
  /** Open state for the "Создать черновик с моими изменениями" CTA. The CTA
   *  is the inline handoff from the disabled-Save path (requiresDraftFlow)
   *  into the schema-draft modal with the maker's in-progress schemaJson
   *  pre-loaded — no retyping. */
  const [draftHandoffOpen, setDraftHandoffOpen] = useState(false)

  // Template (только в create-mode): admin выбирает существующий dict,
  // его schema + locale + scope копируются. Имя остаётся пустым (admin
  // вводит уникальное), version reset на 1.0.0.
  const queryClient = useQueryClient()
  const dictsQuery = useDictionaries()
  const [loadingTemplate, setLoadingTemplate] = useState(false)
  const templateOptions: SelectOption[] = useMemo(() => {
    if (isEdit) return []
    const list = dictsQuery.data ?? []
    return list
      .map((d) => ({
        id: d.name,
        label: d.displayName ? `${d.name} — ${d.displayName}` : d.name,
      }))
      .sort((a, b) => a.label.localeCompare(b.label, 'ru'))
  }, [dictsQuery.data, isEdit])

  const handleApplyTemplate = async (templateName: string) => {
    if (!templateName) return
    setLoadingTemplate(true)
    try {
      const detail = await queryClient.fetchQuery(dictionaryDetailQuery(templateName))
      const tplParsed = parseSchemaJson(detail.schemaJson)
      // Не трогаем name — admin должен ввести уникальное.
      setDisplayName('')
      setDescription(detail.description ?? '')
      setScope(detail.scope)
      setBundle(detail.bundle ?? 'cuod')
      setSchemaVersion('1.0.0')
      setSupportedLocales(detail.supportedLocales ?? ['ru-RU'])
      setDefaultLocale(detail.defaultLocale ?? 'ru-RU')
      setProperties(tplParsed.properties)
      setIdSource(tplParsed.idSource ?? '')
    } finally {
      setLoadingTemplate(false)
    }
  }

  const schemaJson = useMemo(
    () => buildSchemaJson(properties, idSource || undefined),
    [properties, idSource],
  )

  const changes: SchemaChange[] = useMemo(
    () => (isEdit ? diffSchemas(initial?.schemaJson, schemaJson) : []),
    [isEdit, initial, schemaJson],
  )
  const breaking = hasBreakingChanges(changes)
  const suggestedVersion = useMemo(
    () => (isEdit ? suggestVersionBump(initial?.schemaVersion, changes) : schemaVersion),
    [isEdit, initial, changes, schemaVersion],
  )

  // Approval-aware Save: when editing an approval-required dict, any schema
  // change OR toggling approvalRequired off OR changing approvalMinRole has
  // to route through the schema-draft workflow. The backend will reject with
  // 422 approval_required, but it's much better UX to disable Save up front
  // and tell the user to open a draft instead. Mirror the three backend
  // gate clauses exactly so the UX matches the server contract.
  const requiresDraftFlow = computeRequiresDraftFlow({
    isEdit,
    initialApprovalRequired: Boolean(
      (initial as { approvalRequired?: boolean } | null)?.approvalRequired,
    ),
    initialMinRole:
      ((initial as { approvalMinRole?: string } | null)?.approvalMinRole ?? '') || null,
    currentApprovalRequired: approvalRequired,
    currentMinRole: approvalMinRole.trim() === '' ? null : approvalMinRole.trim(),
    schemaChangeCount: changes.length,
  })

  const activeMutation = isEdit ? updateMut : createMut
  const serverError = serverErrorMessage(activeMutation.error)

  const localeOptions: SelectOption[] = DEFAULT_LOCALES.map((l) => ({ id: l, label: l }))
  const defaultLocaleOptions: SelectOption[] = supportedLocales.map((l) => ({ id: l, label: l }))

  const handleSubmit = () => {
    if (!isEdit && !/^[a-z][a-z0-9_]{1,127}$/.test(name)) {
      setNameError(t('schema.nameInvalid'))
      setActiveTab('metadata')
      return
    }
    setNameError(null)
    if (isEdit && breaking) {
      setActiveTab('metadata')
      return
    }

    const payload: CreateDictionaryRequest = {
      name,
      displayName: displayName || undefined,
      description: description || undefined,
      scope,
      schemaJson,
      schemaVersion: schemaVersion || undefined,
      bundle: bundle || undefined,
      supportedLocales: supportedLocales.length > 0 ? supportedLocales : undefined,
      defaultLocale: defaultLocale || undefined,
      redisProjectionEnabled: redisProjection,
      approvalRequired,
      approvalMinRole: approvalMinRole.trim() || undefined,
    }

    if (isEdit) {
      updateMut.mutate(
        { name: initial!.name, payload },
        { onSuccess: () => onSuccess(initial!.name) },
      )
    } else {
      createMut.mutate(payload, { onSuccess: (resp) => onSuccess(resp.name) })
    }
  }

  const tabs: TabItem[] = [
    { id: 'metadata', label: t('schema.tabs.metadata') },
    { id: 'schema', label: t('schema.tabs.schema') },
    { id: 'preview', label: t('schema.tabs.preview') },
    { id: 'events', label: t('schema.tabs.events') },
  ]

  return (
    <Modal
      isOpen={open}
      onClose={onClose}
      title={isEdit ? `${t('schema.action.edit')}: ${initial?.name}` : t('schema.action.create')}
      maxWidth="max-w-5xl"
      panelClassName="max-h-[calc(100vh-2rem)] my-4 flex flex-col"
      bodyClassName="overflow-y-auto flex-1"
    >
      <div className="space-y-4">
        <Tabs items={tabs} value={activeTab} onValueChange={(id) => setActiveTab(id as EditorTab)} />

        <div className={activeTab === 'metadata' ? 'block' : 'hidden'}>
          {!isEdit && (
            <div className="mb-3 p-3 rounded-md border border-accent/20 bg-accent/4">
              <SingleSelect
                label={t('schema.template.label')}
                hint={t('schema.template.hint')}
                options={templateOptions}
                value=""
                onChange={(id) => {
                  void handleApplyTemplate(id)
                }}
                placeholder={
                  dictsQuery.isLoading || loadingTemplate
                    ? '…'
                    : t('schema.template.placeholder')
                }
                disabled={dictsQuery.isLoading || loadingTemplate}
              />
            </div>
          )}

          <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
            <TextInput
              label={t('schema.name')}
              required
              disabled={isEdit}
              placeholder="my_dictionary"
              hint={t('schema.nameHint')}
              error={nameError ?? undefined}
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <TextInput
              label={t('schema.displayName')}
              value={displayName}
              onChange={(e) => setDisplayName(e.target.value)}
            />
            <div className="sm:col-span-2">
              <TextArea
                label={t('schema.description')}
                rows={2}
                value={description}
                onChange={(e) => setDescription(e.target.value)}
              />
            </div>
            <SingleSelect
              label={t('schema.scope')}
              required
              options={SCOPE_OPTIONS(t)}
              value={scope}
              onChange={(id) => setScope(id as DataScope)}
            />
            <TextInput
              label={t('schema.bundle')}
              hint={t('schema.bundleHint')}
              value={bundle}
              onChange={(e) => setBundle(e.target.value)}
            />
            <TextInput
              label={t('schema.version')}
              placeholder="1.0.0"
              hint={
                isEdit && suggestedVersion !== schemaVersion
                  ? t('schema.versionSuggest', { v: suggestedVersion })
                  : undefined
              }
              value={schemaVersion}
              onChange={(e) => setSchemaVersion(e.target.value)}
            />
            <SingleSelect
              label={t('schema.idSource')}
              hint={t('schema.idSourceHint')}
              options={[
                { id: '', label: '— ' + t('schema.idSourceManual') + ' —' },
                ...properties
                  .filter((p) => p.name && (p.kind === 'string' || p.kind === 'integer' || p.kind === 'number'))
                  .map((p) => ({ id: p.name, label: p.name })),
              ]}
              value={idSource}
              onChange={setIdSource}
            />
            <MultiSelect
              label={t('schema.supportedLocales')}
              options={localeOptions}
              value={supportedLocales}
              onChange={(ids) => {
                setSupportedLocales(ids)
                if (!ids.includes(defaultLocale) && ids.length > 0) {
                  setDefaultLocale(ids[0])
                }
              }}
            />
            <SingleSelect
              label={t('schema.defaultLocale')}
              options={defaultLocaleOptions}
              value={defaultLocale}
              onChange={setDefaultLocale}
            />
            {/* CEO plan E2: per-dict Redis projection opt-in. Default false.
                Включается когда dict хочет 10k+ RPS на read-api (вместо PG). */}
            <label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-line bg-line/20 cursor-pointer">
              <input
                type="checkbox"
                checked={redisProjection}
                onChange={(e) => setRedisProjection(e.target.checked)}
                className="mt-0.5 size-4 accent-ultramarain"
              />
              <span className="flex-1">
                <span className="block text-body font-sans text-ink">
                  {t('schema.redisProjection.label')}
                </span>
                <span className="block text-cell text-mute mt-0.5">
                  {t('schema.redisProjection.hint')}
                </span>
              </span>
            </label>

            {/* Approval Workflow v2: per-dict opt-in. Default false. Включается
                для критичных dicts (ground_station, spacecraft и т.д.) где
                один admin не должен иметь power публиковать без review. */}
            <label className="md:col-span-2 flex items-start gap-3 px-3 py-2 rounded-sm border border-warn bg-warn-bg cursor-pointer">
              <input
                type="checkbox"
                checked={approvalRequired}
                onChange={(e) => setApprovalRequired(e.target.checked)}
                className="mt-0.5 size-4 accent-orbit"
              />
              <span className="flex-1">
                <span className="block text-body font-sans text-ink">
                  {t('schema.approvalRequired.label')}
                </span>
                <span className="block text-cell text-mute mt-0.5">
                  {t('schema.approvalRequired.hint')}
                </span>
              </span>
            </label>

            {approvalRequired && (
              <div className="md:col-span-2">
                <label
                  htmlFor="approval-min-role"
                  className="text-cap text-ink-2"
                >
                  {t('schema.approvalMinRole.label')}
                </label>
                <TextInput
                  id="approval-min-role"
                  value={approvalMinRole}
                  onChange={(e) => setApprovalMinRole(e.target.value)}
                  placeholder="ordinis:internal"
                />
                <p className="text-cell text-mute mt-0.5">
                  {t('schema.approvalMinRole.hint')}
                </p>
              </div>
            )}
          </div>
        </div>

        <div className={activeTab === 'schema' ? 'block' : 'hidden'}>
          <SchemaBuilder properties={properties} onChange={setProperties} />
        </div>

        <div className={activeTab === 'preview' ? 'block' : 'hidden'}>
          <pre className="text-mono bg-line/30 rounded p-3 overflow-x-auto whitespace-pre-wrap">
            {JSON.stringify(schemaJson, null, 2)}
          </pre>
        </div>

        <div className={activeTab === 'events' ? 'block' : 'hidden'}>
          {/* Phase 4.5 / CEO plan v1: integrators видят shape Kafka events
              до published'а dictionary. Sample data на основе schema fields. */}
          <EventsPreviewTab
            dictionaryName={name}
            dataScope={scope}
            bundle={bundle}
            schema={schemaJson}
            businessKeyField={idSource || undefined}
          />
        </div>

        {isEdit && breaking && (
          <Alert variant="error" title={t('schema.diff.blockedTitle')}>
            <div className="space-y-2">
              <p className="text-body">{t('schema.diff.blockedBody')}</p>
              <ul className="list-disc pl-5 text-body space-y-0.5">
                {changes
                  .filter((c) => c.severity === 'breaking')
                  .map((c, i) => (
                    <li key={i}>
                      <span className="font-mono">{c.field}</span> — {c.reason}
                    </li>
                  ))}
              </ul>
            </div>
          </Alert>
        )}

        {isEdit && !breaking && changes.length > 0 && (
          <Alert variant="info" title={t('schema.diff.compatibleTitle')}>
            <ul className="list-disc pl-5 text-body space-y-0.5">
              {changes.map((c, i) => (
                <li key={i}>
                  <span className="font-mono">{c.field}</span> — {c.reason}
                </li>
              ))}
            </ul>
          </Alert>
        )}

        {serverError && <Alert variant="error">{serverError}</Alert>}

        {requiresDraftFlow && (
          <Alert
            variant="warning"
            title={t('schema.approvalRequired.editorBlockTitle', {
              defaultValue: 'Изменения требуют ревью',
            })}
            action={
              <Button
                type="button"
                variant="primary"
                size="sm"
                onClick={() => setDraftHandoffOpen(true)}
              >
                {t('schema.approvalRequired.openDraftCta', {
                  defaultValue: 'Создать черновик с моими изменениями',
                })}
              </Button>
            }
          >
            {t('schema.approvalRequired.editorBlock', {
              defaultValue:
                'Этот словарь требует maker-checker review. Откройте черновик — ' +
                'ваши текущие изменения будут предзаполнены, вам нужно только ' +
                'указать причину и отправить на согласование.',
            })}
          </Alert>
        )}

        <FormActions align="end">
          <Button type="button" variant="ghost" onClick={onClose} disabled={activeMutation.isPending}>
            {t('form.cancel')}
          </Button>
          <Button
            type="button"
            variant="primary"
            loading={activeMutation.isPending}
            disabled={(isEdit && breaking) || requiresDraftFlow}
            onClick={handleSubmit}
          >
            {activeMutation.isPending ? t('form.saving') : t('form.save')}
          </Button>
        </FormActions>
      </div>
      {/* Inline handoff: CreateSchemaDraftModal mounted with the in-progress
          schemaJson pre-loaded. Maker hits "Создать черновик с моими изменениями"
          → modal opens with the same edits they had → they add a reason and
          submit. No retyping, no hunt for a button on the dict page. */}
      {isEdit && initial && (
        <CreateSchemaDraftModal
          open={draftHandoffOpen}
          onClose={() => setDraftHandoffOpen(false)}
          detail={initial}
          initialProposedSchema={schemaJson}
          onCreated={() => {
            // Cascade close: dismiss the draft modal AND the editor. Once
            // the draft is submitted, the in-memory edit in the editor is
            // stale by design — the draft is now the source of truth until
            // a reviewer decides. Leaving the editor open would let the
            // maker silently mutate further state that can't go anywhere
            // (Save still disabled until the draft resolves).
            setDraftHandoffOpen(false)
            onClose()
          }}
        />
      )}
    </Modal>
  )
}

const serverErrorMessage = (err: unknown): string | null => {
  if (!err) return null
  if (axios.isAxiosError(err)) {
    const data = err.response?.data as { message?: string; error?: string } | undefined
    return data?.message ?? data?.error ?? err.message
  }
  return err instanceof Error ? err.message : 'Unknown error'
}
