import { TguApiError } from '../model/tguTypes'
import { apiFetch } from './apiFetch'

/**
 * Вызовы версионирования/выдачи плана в pcp-tgu-service напрямую через Vite-прокси
 * (`/api/pcp-tgu` → :7011), как и pcp-mission напрямую на :7010. BFF не используется.
 */
const BASE = '/api/pcp-tgu'

/** Режим наполнения черновика; та же строка уходит в clone миссии. */
export type DraftMode = 'replace' | 'fork-forward'

export type CreateDraftRequest = {
  mode: DraftMode
  /** Начало окна целевой ЗРВ (UTC ISO). Обязательно для fork-forward. */
  windowStart?: string
  /** Конец окна целевой ЗРВ. Обязательно для fork-forward. */
  windowStop?: string
  /** КПП целевой ЗРВ. Обязательно для fork-forward. */
  windowKppId?: string
}

export type CreateDraftResponse = {
  /** Идентификатор созданного черновика (= mission_id клон-миссии). */
  draftPlanId: string
}

export type PromoteDraftResponse = {
  processInstanceKey: number
  processDefinitionKey: number
  bpmnProcessId: string
  version: number
  planId: string
  attemptId: string
}

/** План КА из проекции pcp-tgu-service (persisted ∪ будущая проекция). Времена — ISO с `Z`. */
export type TguPlanResponse = {
  planId: string
  spacecraftId: string
  startTime: string
  endTime: string
  kppId: string
  status: string
}

/** «Сырое» окно ЗРВ КА (граница реальной зоны) из pcp-tgu-service. Времена — ISO с `Z`. */
export type ZrvWindowResponse = {
  startTime: string
  endTime: string
  kppId: string
}

/**
 * Запрос создания плана новой цепочки (вкладка «Планы»). Окно набирается из двух ЗРВ:
 * `windowStart`/`windowKppId` — старт выбранной стартовой зоны, `windowStop` — конец конечной.
 * Времена отправляются в API как UTC ISO с `Z`. `basePlanId` — предыстория
 * (история тянется) либо `null` (цепочка без истории, пустой план).
 */
export type CreateChainPlanRequest = {
  spacecraftId: string
  basePlanId: string | null
  windowStart: string
  windowStop: string
  windowKppId: string
}

export type CreateChainPlanResponse = {
  draftPlanId: string
  /** Прогноз будущей цепочки от созданного плана (what-if, состояние КА не тронуто). */
  projectedChain: TguPlanResponse[]
}

/**
 * Цепочка планов одного КА из pcp-tgu-service (GET /api/plans/{spacecraftId}): persisted планы плюс
 * будущая проекция (PLANNED). Используется fork-forward-пикером для выбора целевой ЗРВ — бэк сверяет
 * выбранное окно ровно с этой же проекцией (assertWindowMatchesProjection).
 * @param spacecraftId Идентификатор КА.
 *
 * @returns Цепочка планов КА (persisted ∪ будущая проекция).
 */
export async function fetchSpacecraftPlans(spacecraftId: string): Promise<TguPlanResponse[]> {
  const url = `${BASE}/api/plans/${encodeURIComponent(spacecraftId)}`
  let response: Response
  try {
    response = await apiFetch(url, { method: 'GET', headers: { Accept: 'application/json' } })
  } catch {
    throw new TguApiError('Не удалось подключиться к сервису планирования ТГУ.', 'network')
  }
  const text = await response.text()
  if (!response.ok) {
    throw new TguApiError(
      extractErrorText(text) || 'Не удалось получить планы КА.',
      'http',
      response.status,
    )
  }
  return text.trim() ? (JSON.parse(text) as TguPlanResponse[]) : []
}

/**
 * «Сырые» ЗРВ КА (границы реальных зон) из pcp-tgu-service (GET /api/plans/{spacecraftId}/zrv).
 * Из них оператор набирает окно нового плана и видит доступные зоны на таймлайне.
 * @param spacecraftId Идентификатор КА.
 *
 * @returns Список «сырых» окон ЗРВ КА (границы реальных зон).
 */
export async function fetchZrvWindows(spacecraftId: string): Promise<ZrvWindowResponse[]> {
  const url = `${BASE}/api/plans/${encodeURIComponent(spacecraftId)}/zrv`
  let response: Response
  try {
    response = await apiFetch(url, { method: 'GET', headers: { Accept: 'application/json' } })
  } catch {
    throw new TguApiError('Не удалось подключиться к сервису планирования ТГУ.', 'network')
  }
  const text = await response.text()
  if (!response.ok) {
    throw new TguApiError(
      extractErrorText(text) || 'Не удалось получить ЗРВ КА.',
      'http',
      response.status,
    )
  }
  return text.trim() ? (JSON.parse(text) as ZrvWindowResponse[]) : []
}

/**
 * Черновики (DRAFT) из pcp-tgu-service (GET /api/plans/drafts), опц. по одному КА. Основной
 * read-path планов их отсекает, поэтому режим «Черновики» во вкладке «Планы» читает их отдельно.
 * @param spacecraftId Идентификатор КА для фильтра (необязателен — иначе все черновики).
 *
 * @returns Список планов-черновиков (DRAFT).
 */
export async function fetchDraftPlans(spacecraftId?: string): Promise<TguPlanResponse[]> {
  const url = spacecraftId
    ? `${BASE}/api/plans/drafts?spacecraftId=${encodeURIComponent(spacecraftId)}`
    : `${BASE}/api/plans/drafts`
  let response: Response
  try {
    response = await apiFetch(url, { method: 'GET', headers: { Accept: 'application/json' } })
  } catch {
    throw new TguApiError('Не удалось подключиться к сервису планирования ТГУ.', 'network')
  }
  const text = await response.text()
  if (!response.ok) {
    throw new TguApiError(
      extractErrorText(text) || 'Не удалось получить черновики.',
      'http',
      response.status,
    )
  }
  return text.trim() ? (JSON.parse(text) as TguPlanResponse[]) : []
}

/**
 * Создаёт план новой цепочки (pcp-tgu-service, POST /api/plans/new-chain). Возвращает план-кандидат
 * (DRAFT) и прогноз будущей цепочки от него. Кандидат становится рабочим после ручной отправки
 * (promote) и приёмки. 404 — базы нет; 422 — окно не из ЗРВ / база чужого КА / нечисловой КА.
 * @param request Параметры новой цепочки (КА, база, окно из двух ЗРВ).
 *
 * @returns План-кандидат (DRAFT) и прогноз будущей цепочки от него.
 */
export async function createChainPlan(
  request: CreateChainPlanRequest,
): Promise<CreateChainPlanResponse> {
  return postJson<CreateChainPlanResponse>(
    `${BASE}/api/plans/new-chain`,
    request,
    'Не удалось создать план новой цепочки.',
  )
}

/**
 * Создаёт черновик плана (DRAFT) на основе базового плана
 * (pcp-tgu-service, POST /api/plans/{basePlanId}/draft). Триггерит клон миссии; возвращает
 * draftPlanId, в который адресуются правки (Ф3) и который применяется promote (Ф5).
 * @param basePlanId Идентификатор базового плана, от которого создаётся черновик.
 * @param request Режим наполнения черновика (replace/fork-forward) и окно целевой ЗРВ.
 *
 * @returns Идентификатор созданного черновика (= mission_id клон-миссии).
 */
export async function createDraft(
  basePlanId: string,
  request: CreateDraftRequest,
): Promise<CreateDraftResponse> {
  return postJson<CreateDraftResponse>(
    `${BASE}/api/plans/${encodeURIComponent(basePlanId)}/draft`,
    request,
    'Не удалось создать черновик плана.',
  )
}

/**
 * «Применить» черновик: промоутит DRAFT в живую цепочку и выдаёт его
 * (pcp-tgu-service, POST /api/plans/{draftPlanId}/promote). Под оптимистичным локом базы и
 * гардом точки невозврата: 409 — база уехала / уже идёт выдача; 422 — не DRAFT / слот сместился /
 * заморожено; 404 — черновика нет.
 * @param draftPlanId Идентификатор черновика для применения.
 *
 * @returns Данные запущенного процесса выдачи (ключи процесса, planId, attemptId).
 */
export async function promoteDraft(draftPlanId: string): Promise<PromoteDraftResponse> {
  return postJson<PromoteDraftResponse>(
    `${BASE}/api/plans/${encodeURIComponent(draftPlanId)}/promote`,
    undefined,
    'Не удалось применить черновик плана.',
  )
}

/**
 * Отказ от черновика (pcp-tgu-service, DELETE /api/plans/{draftPlanId}): удаляет DRAFT и его
 * клон-миссию, снимая блокировку «один открытый черновик на базу». 404 — нет; 422 — не DRAFT.
 * @param draftPlanId Идентификатор черновика для отмены.
 */
export async function abandonDraft(draftPlanId: string): Promise<void> {
  const url = `${BASE}/api/plans/${encodeURIComponent(draftPlanId)}`
  let response: Response
  try {
    response = await apiFetch(url, { method: 'DELETE', headers: { Accept: 'application/json' } })
  } catch {
    throw new TguApiError('Не удалось подключиться к сервису планирования ТГУ.', 'network')
  }
  if (!response.ok) {
    const text = await response.text()
    throw new TguApiError(
      extractErrorText(text) || 'Не удалось отменить черновик плана.',
      'http',
      response.status,
    )
  }
}

async function postJson<T>(url: string, body: unknown, fallbackMessage: string): Promise<T> {
  let response: Response
  try {
    response = await apiFetch(url, {
      method: 'POST',
      headers:
        body === undefined
          ? { Accept: 'application/json' }
          : { Accept: 'application/json', 'Content-Type': 'application/json' },
      ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    })
  } catch {
    throw new TguApiError('Не удалось подключиться к сервису планирования ТГУ.', 'network')
  }

  const text = await response.text()
  if (!response.ok) {
    throw new TguApiError(extractErrorText(text) || fallbackMessage, 'http', response.status)
  }
  if (!text.trim()) {
    return undefined as T
  }
  return JSON.parse(text) as T
}

function extractErrorText(body: string): string {
  const trimmed = body.trim()
  if (!trimmed) return ''
  try {
    const parsed = JSON.parse(trimmed) as Record<string, unknown>
    for (const field of ['message', 'error', 'reason', 'detail']) {
      const value = parsed[field]
      if (typeof value === 'string' && value.trim()) {
        return value
      }
    }
  } catch {
    return trimmed
  }
  return trimmed
}
