<script setup>
import { computed, ref, onBeforeUnmount, nextTick } from "vue";
import { useForm } from "@tanstack/vue-form";
import InputField from "./InputField.vue";
import { defaults, validate, payload, total, request, clone } from "./model.js";
const props = defineProps({ scenario: Object, backend: Boolean });
const id = props.scenario.id;
const result = ref(null),
  message = ref(""),
  tone = ref("info"),
  step = ref(0),
  mode = ref("200"),
  preserve = ref(true),
  loadFail = ref(false),
  loading = ref(false),
  submitBusy = ref(false),
  navigationBusy = ref(false),
  pendingData = ref(null),
  formEl = ref(null);
let epoch = 0,
  serial = 2;
let controller = new AbortController();
const form = useForm({
  defaultValues: defaults(id),
  validators: {
    onSubmit: ({ value }) => {
      const fields = validate(id, value);
      return Object.keys(fields).length ? { fields } : undefined;
    },
  },
  onSubmitInvalid: async () => {
    message.value = "Исправьте отмеченные поля";
    tone.value = "error";
    await nextTick();
    formEl.value?.querySelector('[aria-invalid="true"]')?.focus();
  },
  onSubmit: async ({ value, formApi }) => {
    const myEpoch = epoch;
    submitBusy.value = true;
    message.value = "";
    result.value = null;
    try {
      const r = await request(props.backend, "save", {
        value: { scenario: id, data: payload(id, value) },
        mode: id === "save" ? mode.value : "200",
        signal: controller.signal,
      });
      if (myEpoch !== epoch) return;
      if (r.status === 200) {
        result.value = r.data.record;
        message.value = "Готово: учебные данные приняты";
        tone.value = "success";
      } else {
        for (const [name, error] of Object.entries(r.data.fields || {}))
          formApi.setFieldMeta(name, (m) => ({
            ...m,
            isTouched: true,
            errorMap: { ...m.errorMap, onServer: error },
          }));
        message.value = `HTTP ${r.status}: ${r.data.message}`;
        tone.value = "error";
        await nextTick();
        formEl.value?.querySelector('[aria-invalid="true"]')?.focus();
      }
    } catch (e) {
      if (e.name !== "AbortError" && myEpoch === epoch) {
        message.value = "Не удалось получить ответ. Ваш ввод сохранён.";
        tone.value = "error";
      }
    } finally {
      if (myEpoch === epoch) submitBusy.value = false;
    }
  },
});
const state = form.useSelector((s) => s);
const values = computed(() => state.value.values);
const sum = computed(() => (id === "invoice" ? total(values.value) : 0));
const currentPayload = computed(() => payload(id, values.value));
function rules(name, extra = {}) {
  const check = ({ value, fieldApi }) =>
    validate(id, fieldApi.form.state.values)[name];
  return {
    onBlur: check,
    onChange: (arg) =>
      arg.fieldApi.state.meta.isTouched ? check(arg) : undefined,
    ...extra,
  };
}
const usernameRules = {
  onChange: ({ value }) => validate("username", { username: value }).username,
  onChangeAsyncDebounceMs: 300,
  onChangeAsync: async ({ value, signal }) => {
    const run = epoch;
    try {
      const r = await request(props.backend, "check", { value, signal });
      if (run !== epoch) return;
      if (r.status !== 200) return "Не удалось проверить имя";
      return r.data.available ? undefined : "Имя занято. Попробуйте marina";
    } catch (e) {
      if (e.name !== "AbortError" && run === epoch)
        return "Ошибка проверки. Повторите ввод";
    }
  },
};
function reset() {
  epoch++;
  controller.abort();
  controller = new AbortController();
  submitBusy.value = false;
  navigationBusy.value = false;
  form.reset(defaults(id));
  result.value = null;
  message.value = "Начальные значения восстановлены";
  tone.value = "info";
  loading.value = false;
  pendingData.value = null;
  step.value = 0;
  serial = 2;
}
function fill() {
  epoch++;
  controller.abort();
  controller = new AbortController();
  submitBusy.value = false;
  navigationBusy.value = false;
  loading.value = false;
  pendingData.value = null;
  form.reset({
    ...defaults(id),
    name: "Марина Соколова",
    email: "marina@example.test",
    ...(id === "profile" || id === "load"
      ? { contact: { phone: "+7 900 555-12-34" } }
      : {}),
  });
  message.value = "Пример заполнен. Измените любое поле";
  tone.value = "info";
  result.value = null;
}
function typeChanged(event) {
  form.setFieldValue("type", event.target.value);
  if (event.target.value === "person" && !preserve.value) {
    form.setFieldValue("company", { name: "", taxId: "" });
  }
  for (const name of ["company.name", "company.taxId"])
    form.setFieldMeta(name, (m) => ({ ...m, errorMap: {} }));
}
function addItem(field) {
  serial += 1;
  field.pushValue({ id: "line-" + serial, title: "", qty: 1, price: 0 });
}
async function load() {
  if (loading.value) return;
  const run = epoch;
  loading.value = true;
  message.value = "";
  pendingData.value = null;
  const before = JSON.stringify(form.state.values);
  try {
    const r = await request(props.backend, "load", {
      mode: loadFail.value ? "500" : "200",
      signal: controller.signal,
    });
    if (run !== epoch) return;
    if (r.status !== 200) {
      message.value = r.data.message;
      tone.value = "error";
      return;
    }
    if (form.state.isDirty || JSON.stringify(form.state.values) !== before) {
      pendingData.value = r.data;
      message.value = "Есть ваш ввод. Загруженная карточка ждёт решения ниже.";
      tone.value = "info";
    } else {
      form.reset(r.data);
      message.value = "Клиент загружен";
      tone.value = "success";
    }
  } catch (e) {
    if (e.name !== "AbortError" && run === epoch) {
      message.value = "Загрузка недоступна. Повторите запрос";
      tone.value = "error";
    }
  } finally {
    if (run === epoch) loading.value = false;
  }
}
function applyLoaded() {
  form.reset(clone(pendingData.value));
  pendingData.value = null;
  message.value = "Вы заменили форму загруженной карточкой";
  tone.value = "success";
}
async function next() {
  if (navigationBusy.value || step.value >= 2) return;
  navigationBusy.value = true;
  const run = epoch,
    fromStep = step.value;
  const names = fromStep === 0 ? ["name", "email"] : ["service", "people"];
  try {
    for (const name of names) {
      await form.validateField(name, "blur");
      if (run !== epoch || step.value !== fromStep) return;
    }
    const errors = validate(id, values.value);
    for (const name of names) {
      if (errors[name])
        form.setFieldMeta(name, (m) => ({
          ...m,
          isTouched: true,
          errorMap: { ...m.errorMap, onBlur: errors[name] },
        }));
    }
    if (names.some((n) => errors[n])) {
      message.value = "Заполните текущий шаг";
      tone.value = "error";
      await nextTick();
      if (run === epoch && step.value === fromStep)
        formEl.value?.querySelector('[aria-invalid="true"]')?.focus();
      return;
    }
    step.value = fromStep + 1;
    message.value = "";
  } finally {
    if (run === epoch) navigationBusy.value = false;
  }
}
function previous() {
  if (navigationBusy.value) return;
  step.value = Math.max(0, step.value - 1);
  message.value = "";
}
function exportJson() {
  const blob = new Blob([JSON.stringify(currentPayload.value, null, 2)], {
      type: "application/json",
    }),
    url = URL.createObjectURL(blob),
    a = document.createElement("a");
  a.href = url;
  a.download = `tanstack-form-${id}.json`;
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
  message.value =
    "JSON текущих значений подготовлен. Это данные, а не подтверждение сервера.";
  tone.value = "info";
}
onBeforeUnmount(() => {
  epoch++;
  controller.abort();
});
</script>
<template>
  <section class="lab-card">
    <div class="section-intro">
      <span class="eyebrow">{{ scenario.concept }}</span>
      <h2>{{ scenario.title }}</h2>
      <p>{{ scenario.use }}</p>
      <div class="try-hint">
        <strong>Попробуйте:</strong> {{ scenario.hint }}
      </div>
    </div>
    <div class="form-layout">
      <form
        ref="formEl"
        novalidate
        @submit.prevent.stop="!submitBusy && form.handleSubmit()"
        class="form-panel"
      >
        <div v-if="id === 'wizard'" class="steps" aria-label="Шаги заявки">
          <span
            v-for="(label, i) in ['Контакт', 'Услуга', 'Проверка']"
            :key="label"
            :class="{ current: step === i }"
            :aria-current="step === i ? 'step' : undefined"
            >{{ i + 1 }}. {{ label }}</span
          >
        </div>
        <template
          v-if="
            ['profile', 'company', 'load', 'save'].includes(id) ||
            (id === 'wizard' && step === 0)
          "
          ><InputField
            :form="form"
            name="name"
            label="Имя клиента"
            :validators="rules('name')" /><InputField
            :form="form"
            name="email"
            label="Электронная почта"
            type="email"
            help="Учебный адрес: marina@example.test"
            :validators="rules('email')"
        /></template>
        <InputField
          v-if="['profile', 'load'].includes(id)"
          :form="form"
          name="contact.phone"
          label="Телефон (необязательно)"
          type="tel"
          help="Вложенное поле contact.phone"
        />
        <template v-if="id === 'company'"
          ><label for="applicant-type" class="form-label"
            >Кто подаёт заявку</label
          ><select
            id="applicant-type"
            class="form-select mb-3"
            :value="values.type"
            @change="typeChanged"
          >
            <option value="person">Частное лицо</option>
            <option value="company">Организация</option></select
          ><label class="check-row"
            ><input
              v-model="preserve"
              type="checkbox"
              class="form-check-input"
            />
            Сохранять скрытые реквизиты в форме</label
          >
          <div v-if="values.type === 'company'" class="nested-panel">
            <InputField
              :form="form"
              name="company.name"
              label="Название организации"
              :validators="rules('company.name')"
            /><InputField
              :form="form"
              name="company.taxId"
              label="ИНН"
              help="В демо проверяются только цифры и длина, не контрольная сумма."
              :validators="rules('company.taxId')"
            />
          </div>
          <p v-else class="field-help">
            Реквизиты скрыты и не попадут в отправляемый payload частного лица.
          </p></template
        >
        <template v-if="id === 'invoice'"
          ><InputField
            :form="form"
            name="customer"
            label="Заказчик"
            :validators="rules('customer')"
          /><component :is="form.Field" name="items" mode="array"
            ><template #default="{ field }"
              ><article
                v-for="(item, i) in field.state.value"
                :key="item.id"
                class="invoice-row"
              >
                <div class="row-heading">
                  <strong>Позиция {{ i + 1 }}</strong
                  ><small>{{ item.id }}</small>
                </div>
                <InputField
                  :key="item.id + '-title-' + i"
                  :uid="item.id + '-title'"
                  :form="form"
                  :name="`items[${i}].title`"
                  label="Услуга"
                  :validators="rules(`items[${i}].title`)"
                />
                <div class="two-cols">
                  <InputField
                    :key="item.id + '-qty-' + i"
                    :uid="item.id + '-qty'"
                    :form="form"
                    :name="`items[${i}].qty`"
                    label="Количество"
                    type="number"
                    :validators="rules(`items[${i}].qty`)"
                  /><InputField
                    :key="item.id + '-price-' + i"
                    :uid="item.id + '-price'"
                    :form="form"
                    :name="`items[${i}].price`"
                    label="Цена, ₽"
                    type="number"
                    :validators="rules(`items[${i}].price`)"
                  />
                </div>
                <div class="row-actions">
                  <button
                    type="button"
                    class="btn btn-sm btn-outline-secondary"
                    :disabled="i === 0"
                    @click="field.moveValue(i, i - 1)"
                    :aria-label="`Выше: позиция ${i + 1}`"
                  >
                    ↑ Выше</button
                  ><button
                    type="button"
                    class="btn btn-sm btn-outline-danger"
                    :disabled="field.state.value.length === 1"
                    @click="field.removeValue(i)"
                    :aria-label="`Удалить позицию ${i + 1}`"
                  >
                    Удалить
                  </button>
                </div>
              </article>
              <button
                type="button"
                class="btn btn-outline-success"
                :disabled="field.state.value.length >= 10"
                @click="addItem(field)"
              >
                Добавить позицию
              </button>
              <p class="field-error">
                {{ field.state.meta.errors.join(", ") }}
              </p></template
            ></component
          >
          <div class="total">
            Итого <strong>{{ sum.toLocaleString("ru-RU") }} ₽</strong>
          </div></template
        >
        <template v-if="id === 'booking'"
          ><InputField
            :form="form"
            name="start"
            label="Начало"
            type="date"
            :validators="rules('start')" /><InputField
            :form="form"
            name="end"
            label="Окончание"
            type="date"
            :validators="
              rules('end', {
                onBlur: undefined,
                onChangeListenTo: ['start'],
                onChange: ({ fieldApi }) =>
                  validate(id, fieldApi.form.state.values).end,
              })
            " /><InputField
            :form="form"
            name="people"
            label="Участники"
            type="number"
            help="От 1 до 20. Пустое поле остаётся пустой строкой, 0 — числом."
            :validators="rules('people')"
        /></template>
        <InputField
          v-if="id === 'username'"
          :form="form"
          name="username"
          label="Имя пользователя"
          help="admin, root, support заняты. Например: marina_7."
          :validators="usernameRules"
        />
        <div v-if="id === 'load'" class="load-tools">
          <label class="check-row"
            ><input
              v-model="loadFail"
              type="checkbox"
              class="form-check-input"
            />
            Учебная ошибка загрузки</label
          ><button
            type="button"
            class="btn btn-outline-success"
            :disabled="loading"
            @click="load"
          >
            {{ loading ? "Загружаем…" : "Загрузить клиента" }}
          </button>
          <div v-if="pendingData" class="pending-box">
            <p>
              Загружено: <strong>{{ pendingData.name }}</strong> ·
              {{ pendingData.email }}
            </p>
            <button
              type="button"
              class="btn btn-sm btn-success"
              @click="applyLoaded"
            >
              Заменить мою форму</button
            ><button
              type="button"
              class="btn btn-sm btn-outline-secondary"
              @click="
                pendingData = null;
                message = 'Ваш ввод сохранён';
              "
            >
              Оставить мой ввод
            </button>
          </div>
        </div>
        <div v-if="id === 'save'">
          <label for="response-mode" class="form-label"
            >Ответ учебного API</label
          ><select id="response-mode" v-model="mode" class="form-select mb-3">
            <option value="200">200 — принято</option>
            <option value="422">422 — ошибка почты</option>
            <option value="409">409 — конфликт версии</option>
            <option value="500">500 — временный сбой</option>
          </select>
        </div>
        <template v-if="id === 'wizard' && step === 1"
          ><InputField
            :form="form"
            name="service"
            label="Услуга"
            :options="['Консультация', 'Настройка сервиса', 'Аудит интерфейса']"
            :validators="rules('service')" /><InputField
            :form="form"
            name="people"
            label="Участники"
            type="number"
            :validators="rules('people')"
        /></template>
        <div v-if="id === 'wizard' && step === 2" class="summary-box">
          <h3>Проверьте заявку</h3>
          <dl>
            <dt>Клиент</dt>
            <dd>{{ values.name }} · {{ values.email }}</dd>
            <dt>Услуга</dt>
            <dd>{{ values.service }}</dd>
            <dt>Участники</dt>
            <dd>{{ values.people }}</dd>
          </dl>
          <InputField
            :form="form"
            name="consent"
            label="Подтверждаю состав учебной заявки"
            type="checkbox"
            :validators="rules('consent')"
          />
        </div>
        <div class="actions">
          <button
            v-if="id === 'wizard' && step > 0"
            type="button"
            class="btn btn-outline-secondary"
            :disabled="navigationBusy"
            @click="previous"
          >
            Назад</button
          ><button
            v-if="id === 'wizard' && step < 2"
            type="button"
            class="btn btn-success"
            :disabled="navigationBusy"
            @click="next"
          >
            Далее</button
          ><button
            v-else
            type="submit"
            class="btn btn-success"
            :disabled="
              submitBusy || state.isSubmitting || state.isValidating || loading
            "
          >
            {{
              submitBusy || state.isSubmitting
                ? "Отправляем…"
                : "Проверить и отправить"
            }}</button
          ><button
            type="button"
            class="btn btn-outline-secondary"
            @click="reset"
          >
            Сбросить</button
          ><button
            v-if="['profile', 'load'].includes(id)"
            type="button"
            class="btn btn-outline-success"
            @click="fill"
          >
            Заполнить пример
          </button>
        </div>
        <div v-if="message" :class="['status-box', tone]" role="status">
          {{ message }}
        </div>
      </form>
      <aside class="inspector" aria-label="Состояние формы">
        <h3>Что хранит форма</h3>
        <div class="state-badges">
          <span>{{
            state.isDirty ? "Есть изменения" : "Исходные значения"
          }}</span><span>{{
            state.isTouched ? "Поля трогали" : "Поля не трогали"
          }}</span
          ><span>{{
            submitBusy || state.isSubmitting
              ? "Отправка идёт"
              : "Отправка не идёт"
          }}</span>
        </div>
        <p>
          Текущий payload. Изменения видны сразу; скрытые реквизиты частного
          лица удалены.
        </p>
        <pre>{{ JSON.stringify(currentPayload, null, 2) }}</pre>
        <button
          type="button"
          class="btn btn-outline-success"
          @click="exportJson"
        >
          Скачать JSON
        </button>
        <details class="result-details" :open="result !== null">
          <summary>Результат отправки</summary>
          <pre v-if="result !== null">{{
            JSON.stringify(result, null, 2)
          }}</pre>
          <p v-else>Результат появится после успешного ответа.</p>
        </details>
        <details>
          <summary>Код этого интерфейса</summary>
          <p>
            Все восемь вариантов находятся в
            <a
              href="./source/ScenarioForm.vue.txt"
              target="_blank"
              rel="noopener"
              >ScenarioForm.vue</a
            >. Правила и фикстура API — в
            <a href="./source/model.js" target="_blank" rel="noopener"
              >model.js</a
            >.
          </p>
        </details>
      </aside>
    </div>
  </section>
</template>
