const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:4100";

function encodePathname(value: string) {
  return value
    .split("/")
    .filter(Boolean)
    .map((part) => encodeURIComponent(part))
    .join("/");
}

export type HealthResponse = {
  status: string;
  service: string;
};

export type ProjectSummary = {
  id: string;
  name: string;
  slug: string;
  createdAt: string;
  updatedAt: string;
  source: {
    id: string;
    type: string;
    path: string | null;
    config: Record<string, unknown>;
    createdAt: string;
    updatedAt: string;
  } | null;
};

export type ProjectScanSummary = {
  id: string;
  projectId: string;
  sourceId: string;
  status: string;
  projectIndexKey: string | null;
  startedAt: string | null;
  completedAt: string | null;
  errorMessage: string | null;
  createdAt: string;
  counts: {
    pages: number;
    assets: number;
  };
  audit: {
    runId: string;
    status: string;
    startedAt: string | null;
    completedAt: string | null;
    errorMessage: string | null;
    createdAt: string;
    scanVersionId: string;
    summary: {
      pagesChecked: number;
      assetsChecked: number;
      missingTitle: number;
      missingDescription: number;
      missingH1: number;
      mediaWarnings: number;
    };
    counts: {
      total: number;
      errors: number;
      warnings: number;
      info: number;
    };
    issues: Array<{
      id: string;
      severity: "error" | "warning" | "info";
      scope: "page" | "media" | "project";
      code: string;
      message: string;
      pageId?: string;
      mediaId?: string;
      sourcePath?: string;
      urlPath?: string | null;
      field?: string;
    }>;
  } | null;
  pages: Array<{
    pageId: string;
    selected: boolean;
    sourcePath: string;
    urlPath: string | null;
    title: string | null;
    description: string | null;
    fingerprint: string | null;
    reconcileStatus: string;
    scope: "indexable_page" | "template_block" | "admin_page" | "technical_page";
    scopeReason: string;
    previewSourcePath: string | null;
    previewTargetSelector: string | null;
    previewKind: "page" | "home_section" | "raw_template" | "none";
    structureLabel: string | null;
    structureOrder: number | null;
    visibleInSite: boolean | null;
    usedForCoverage: boolean;
    headings: Array<{ level: number; text: string }>;
  }>;
  siteSections: Array<{
    sectionId: string;
    pageId: string | null;
    selected: boolean;
    title: string;
    sourcePath: string;
    template: string;
    enabled: boolean;
    order: number;
    regionId: string | null;
    regionLabel: string | null;
    previewSourcePath: string | null;
    previewTargetSelector: string | null;
    previewTargetIndex: number | null;
    previewKind: "page" | "home_section" | "raw_template" | "none";
    scopeReason: string;
    headings: Array<{ level: number; text: string }>;
  }>;
  assets: Array<{
    mediaId: string;
    sourcePath: string;
    mediaType: string;
    size: number | null;
    reconcileStatus: string;
  }>;
};

export type ScopeFlowLayout = {
  version: number;
  viewport?: {
    x: number;
    y: number;
    zoom: number;
  };
  nodes: Record<
    string,
    {
      height?: number;
      position: {
        x: number;
        y: number;
      };
      width?: number;
    }
  >;
};

export type SemanticAnalysisRun = {
  schemaVersion: "semantic-analysis.v1";
  runId: string;
  status: string;
  startedAt: string | null;
  completedAt: string | null;
  errorMessage: string | null;
  createdAt: string;
  projectId: string;
  scanVersionId: string;
  sourceId: string;
  projectIndexKey: string | null;
  generatedAt: string;
  summary: {
    coverageScore: number;
    lexicalCoverageScore?: number;
    evidenceScore?: number;
    intentCoverageScore?: number;
    commercialReadinessScore?: number;
    coveredClusters: number;
    weakClusters: number;
    missingClusters: number;
    legacyFindings: number;
    pagesAnalyzed: number;
    selectedPagesAnalyzed: number;
    contentSourcesAnalyzed: number;
    indexablePagesAnalyzed: number;
    excludedHtmlDocuments: number;
    qualityProblems: number;
    qualityWarnings: number;
    seedCandidates: number;
  };
  pageScope: {
    indexablePages: number;
    selectedIndexablePages?: number;
    unselectedIndexablePages?: number;
    templateBlocks: number;
    adminPages: number;
    technicalPages: number;
    contentSources: number;
    coverageDocuments: number;
    excludedDocuments: number;
  };
  styleProfile: {
    language: "ru" | "mixed" | "unknown";
    averageSentenceWords: number;
    topTerms: Array<{
      term: string;
      count: number;
    }>;
    brandTerms: string[];
    ctaPhrases: string[];
    toneSignals: string[];
  };
  qualitySignals: Array<{
    id: string;
    level: "ok" | "warning" | "problem";
    title: string;
    message: string;
  }>;
  analysisVerdict?: {
    confidence: "low" | "medium" | "high";
    marketObjectivity: "internal_only" | "partial" | "market_ready";
    summary: string;
    whatWorks: string[];
    limits: string[];
    externalSignalsNeeded: string[];
    risks: Array<{
      id: string;
      level: "warning" | "problem";
      title: string;
      message: string;
    }>;
    clusterAssessments: Array<{
      clusterId: string;
      title: string;
      assessment: "confirmed_in_content" | "weak_in_content" | "missing_in_content" | "needs_market_validation";
      needsMarketValidation: boolean;
      reason: string;
    }>;
    nextValidationStep: string;
  };
  clusters: Array<{
    id: string;
    title: string;
    priority: "high" | "medium" | "low";
    targetIntent: string;
    status: "covered" | "weak" | "missing";
    score: number;
    hits: number;
    matchedTerms: string[];
    missingTerms: string[];
    queryExamples: string[];
    targetPages: string[];
    evidenceSnippets: Array<{
      sourceId: string;
      sourceKind: "page" | "content_json";
      sourcePath: string;
      title: string;
      text: string;
      matchedTerms: string[];
    }>;
    sourceRefs: Array<{
      id: string;
      kind: "page" | "content_json";
      scope: "indexable_page" | "template_block" | "admin_page" | "technical_page" | "content_source";
      title: string;
      sourcePath: string;
      urlPath: string | null;
      hits: number;
    }>;
    evidence?: {
      level: "none" | "thin" | "moderate" | "strong";
      score: number;
      lexicalScore: number;
      intentScore: number;
      landingPageHits: number;
      contentSourceHits: number;
      uniqueSources: number;
      matchedIntentGroups: string[];
      missingIntentGroups: string[];
      supportSignals: string[];
      penalties: string[];
    };
  }>;
  pages: Array<{
    pageId: string;
    selected: boolean;
    title: string;
    sourcePath: string;
    urlPath: string | null;
    scope: "indexable_page" | "template_block" | "admin_page" | "technical_page";
    scopeReason: string;
    usedForCoverage: boolean;
    wordCount: number;
    matchedClusters: string[];
    missingCriticalFields: string[];
  }>;
  contentSources: Array<{
    id: string;
    sourcePath: string;
    title: string;
    usedForCoverage: boolean;
    wordCount: number;
    matchedClusters: string[];
  }>;
  legacyFindings: Array<{
    markerId: string;
    label: string;
    severity: "blocker" | "warning";
    sourceId: string;
    sourceKind: "page" | "content_json";
    title: string;
    sourcePath: string;
    urlPath: string | null;
    count: number;
    action: string;
  }>;
  gaps: Array<{
    clusterId: string;
    title: string;
    status: "weak" | "missing";
    priority: "high" | "medium" | "low";
    targetIntent: string;
    targetPages: string[];
    seedExamples: string[];
  }>;
  landingOpportunities?: Array<{
    clusterId: string;
    title: string;
    priority: "high" | "medium" | "low";
    status: "covered" | "weak" | "missing";
    score: number;
    evidenceLevel: "none" | "thin" | "moderate" | "strong";
    targetIntent: string;
    pageType: "conversion" | "deployment" | "docs" | "home" | "integration" | "landing" | "module" | "security" | "use_case";
    action: "create_target_page" | "keep_home" | "strengthen_existing" | "validate_external";
    currentLandingPages: string[];
    targetPages: string[];
    existingTargetPages: string[];
    missingTargetPages: string[];
    recommendedPages: Array<{
      path: string;
      pageType: "conversion" | "deployment" | "docs" | "home" | "integration" | "landing" | "module" | "security" | "use_case";
      action: "create" | "strengthen" | "validate";
      reason: string;
    }>;
    reason: string;
  }>;
  landingPagePlan?: Array<{
    path: string;
    pageType: "conversion" | "deployment" | "docs" | "home" | "integration" | "landing" | "module" | "security" | "use_case";
    action: "create" | "strengthen" | "validate";
    priority: "high" | "medium" | "low";
    readinessScore: number;
    opportunityScore: number;
    coveredClusterCount: number;
    weakClusterCount: number;
    missingClusterCount: number;
    reason: string;
    currentSources: string[];
    seedPhrases: string[];
    clusters: Array<{
      clusterId: string;
      title: string;
      priority: "high" | "medium" | "low";
      status: "covered" | "weak" | "missing";
      score: number;
      evidenceLevel: "none" | "thin" | "moderate" | "strong";
      action: "create_target_page" | "keep_home" | "strengthen_existing" | "validate_external";
      targetIntent: string;
    }>;
  }>;
  landingPageBriefs?: Array<{
    path: string;
    pageType: "conversion" | "deployment" | "docs" | "home" | "integration" | "landing" | "module" | "security" | "use_case";
    action: "create" | "strengthen" | "validate";
    priority: "high" | "medium" | "low";
    h1: string;
    title: string;
    metaDescription: string;
    targetIntent: string;
    audience: string;
    primaryCta: string;
    secondaryCta: string;
    introAngle: string;
    mustUseTerms: string[];
    seedPhrases: string[];
    internalLinks: string[];
    sections: Array<{
      title: string;
      goal: string;
      mustCoverTerms: string[];
    }>;
    readinessChecklist: Array<{
      id: string;
      title: string;
      done: boolean;
      reason: string;
    }>;
    clusters: Array<{
      clusterId: string;
      title: string;
      status: "covered" | "weak" | "missing";
      priority: "high" | "medium" | "low";
      evidenceLevel: "none" | "thin" | "moderate" | "strong";
    }>;
    qualityContract?: {
      version: "brief-quality.v1";
      score: number;
      level: "ready" | "needs_work" | "blocked";
      summary: string;
      nextStep: string;
      metrics: {
        h1Length: number;
        titleLength: number;
        metaDescriptionLength: number;
        clusterCount: number;
        sectionCount: number;
        seedPhraseCount: number;
        mustUseTermCount: number;
        internalLinkCount: number;
        checklistDone: number;
        checklistPending: number;
      };
      checks: Array<{
        id: string;
        title: string;
        status: "pass" | "warn" | "fail";
        reason: string;
      }>;
      riskFlags: Array<{
        id: string;
        level: "warning" | "problem";
        title: string;
        message: string;
      }>;
      missingEvidence: string[];
    };
  }>;
  seedCandidates: Array<{
    phrase: string;
    clusterId: string;
    clusterTitle: string;
    source: "ontology" | "detected";
    reason: string;
    priority: "high" | "medium" | "low";
  }>;
  nextActions: string[];
};

export type SemanticExportFormat = "csv" | "json" | "markdown";

export type MarketEnrichmentContract = {
  schemaVersion: "market-enrichment.v1";
  projectId: string;
  semanticRunId: string | null;
  generatedAt: string;
  state: "not_ready" | "not_collected" | "not_configured" | "partial_ready";
  readiness: {
    canCollectWordstat: boolean;
    canCollectSerp: boolean;
    seedCount: number;
    briefCount: number;
    configuredProviderCount: number;
    userSetupRequired: boolean;
  };
  providers: Array<{
    id: "model" | "serp" | "text_quality" | "wordstat";
    label: string;
    role: string;
    status: "connected" | "disabled" | "not_configured" | "not_implemented";
    mode: string;
    userActionRequired: boolean;
    platformActionRequired: boolean;
    message: string;
  }>;
  seedQueue: Array<{
    phrase: string;
    clusterId: string;
    clusterTitle: string;
    priority: "high" | "medium" | "low";
    source: "detected" | "ontology";
    evidenceStatus:
      | "collection_failed"
      | "collected"
      | "collected_no_signal"
      | "collected_related_only"
      | "deterministic_only"
      | "not_collected"
      | "not_configured"
      | "ready_for_collection";
    targetProviders: Array<"model" | "serp" | "text_quality" | "wordstat">;
    blocker: string | null;
    frequency: number | null;
    frequencyGroup: string | null;
    relatedCount: number;
    lastCollectedAt: string | null;
  }>;
  briefEvidence: Array<{
    path: string;
    priority: "high" | "medium" | "low";
    action: "create" | "strengthen" | "validate";
    qualityScore: number | null;
    qualityLevel: "blocked" | "needs_work" | "ready" | null;
    seedPhrases: string[];
    evidenceStatus: "deterministic_only" | "not_collected" | "not_configured" | "ready_for_collection";
    requiredProviders: Array<"model" | "serp" | "text_quality" | "wordstat">;
    missingEvidence: string[];
  }>;
  wordstat: {
    latestJob: {
      id: string;
      status: "queued" | "running" | "done" | "failed" | "cancelled";
      provider: string;
      mode: string;
      region: string;
      requestedPhraseCount: number;
      rawResultKey: string | null;
      resultCount: number;
      errorMessage: string | null;
      startedAt: string | null;
      completedAt: string | null;
      createdAt: string;
      updatedAt: string;
    } | null;
    seedResults: Array<{
      seedId: string | null;
      phrase: string;
      sourcePhrase: string | null;
      frequency: number | null;
      frequencyGroup: string | null;
      region: string | null;
      relatedCount: number;
      collectedAt: string;
    }>;
    topResults: Array<{
      phrase: string;
      sourcePhrase: string | null;
      frequency: number | null;
      frequencyGroup: string | null;
      region: string | null;
    }>;
    summary: {
      collectedSeedCount: number;
      exactFrequencySeedCount?: number;
      relatedOnlySeedCount?: number;
      noSignalSeedCount?: number;
      resultCount: number;
      totalFrequency: number;
      highFrequencyCount: number;
      midFrequencyCount: number;
      lowFrequencyCount: number;
    };
  };
  nextActions: string[];
};

export type ExternalServiceSettings = {
  schemaVersion: "external-service-settings.v1";
  services: Array<{
    id: "yandex_cloud" | "yandex_metrica" | "yandex_search_serp" | "yandex_webmaster" | "yandex_wordstat";
    label: string;
    description: string;
    implementationStatus: "active" | "planned";
    status: "configured" | "disabled" | "missing_required" | "not_implemented";
    enabled: boolean;
    mode: string;
    dependsOn: Array<"yandex_cloud" | "yandex_metrica" | "yandex_search_serp" | "yandex_webmaster" | "yandex_wordstat">;
    modes: Array<{
      id: string;
      label: string;
    }>;
    docs: Array<{
      label: string;
      url: string;
    }>;
    publicFields: Array<{
      id: string;
      label: string;
      type: "number" | "password" | "text" | "url";
      required: boolean;
      placeholder: string;
      help: string;
    }>;
    secretFields: Array<{
      id: string;
      label: string;
      type: "number" | "password" | "text" | "url";
      required: boolean;
      placeholder: string;
      help: string;
    }>;
    publicConfig: Record<string, string>;
    secrets: Record<
      string,
      {
        configured: boolean;
      }
    >;
    missingFields: string[];
    updatedAt: string | null;
  }>;
};

export type ExternalServiceUpdateInput = {
  enabled: boolean;
  mode: string;
  publicConfig: Record<string, string>;
  secrets?: Record<string, string>;
};

export type PageWorkspace = {
  projectId: string;
  pageId: string;
  page: {
    scanVersionId: string;
    sourcePath: string;
    urlPath: string | null;
    title: string | null;
    description: string | null;
    headings: Array<{ level: number; text: string }>;
  };
  draft: {
    id: string;
    status: string;
    createdAt: string;
    updatedAt: string;
  };
  item: {
    id: string;
    field: string;
    originalSnapshotKey: string | null;
    workingSnapshotKey: string | null;
    originalText: string;
    workingText: string;
    originalTextLength: number;
    workingTextLength: number;
    updatedAt: string;
  };
  previewHtml: string;
  sections: Array<{
    id: string;
    kind: "meta" | "content_block" | "heading_group" | "media";
    title: string;
    selector: string | null;
    originalText: string;
    workingText: string;
    charCount: number;
    summary: string;
    sourceHint: string;
  }>;
};

export type CreateProjectInput = {
  name: string;
  sourcePath: string;
};

export type BrowserFolderFileInput = {
  relativePath: string;
  contentBase64: string;
  contentType?: string;
  size: number;
  lastModified?: number;
};

export type ImportBrowserFolderInput = {
  name: string;
  rootName: string;
  files: BrowserFolderFileInput[];
};

type ApiErrorResponse = {
  error?: {
    message?: string;
  };
};

async function getErrorMessage(response: Response, fallback: string) {
  try {
    const payload = (await response.json()) as ApiErrorResponse;
    return payload.error?.message ?? fallback;
  } catch {
    return fallback;
  }
}

export async function fetchHealth() {
  const response = await fetch(`${apiBaseUrl}/health`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Проверка бэкенда не прошла: ${response.status}`));
  }

  return response.json() as Promise<HealthResponse>;
}

export async function fetchProjects() {
  const response = await fetch(`${apiBaseUrl}/projects`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить проекты: ${response.status}`));
  }

  return response.json() as Promise<{ projects: ProjectSummary[] }>;
}

export async function createProject(input: CreateProjectInput) {
  const response = await fetch(`${apiBaseUrl}/projects`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(input)
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось создать проект: ${response.status}`));
  }

  return response.json() as Promise<{ project: ProjectSummary }>;
}

export async function importBrowserFolder(input: ImportBrowserFolderInput) {
  const response = await fetch(`${apiBaseUrl}/projects/import-folder`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(input)
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось импортировать папку: ${response.status}`));
  }

  return response.json() as Promise<{ project: ProjectSummary }>;
}

export async function renameProject(projectId: string, name: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ name })
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось переименовать проект: ${response.status}`));
  }

  return response.json() as Promise<{ project: ProjectSummary }>;
}

export async function copyProject(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/copy`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось скопировать проект: ${response.status}`));
  }

  return response.json() as Promise<{ project: ProjectSummary }>;
}

export async function fetchLatestProjectScan(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/scans/latest`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить скан проекта: ${response.status}`));
  }

  return response.json() as Promise<{ scan: ProjectScanSummary | null }>;
}

export async function runProjectScan(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/scans`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось просканировать проект: ${response.status}`));
  }

  return response.json() as Promise<{ scan: ProjectScanSummary }>;
}

export async function fetchProjectScopeLayout(projectId: string, layoutKey: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/scope-layouts/${encodeURIComponent(layoutKey)}`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить layout workflow: ${response.status}`));
  }

  return response.json() as Promise<{ layout: ScopeFlowLayout | null }>;
}

export async function saveProjectScopeLayout(projectId: string, layoutKey: string, layout: ScopeFlowLayout) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/scope-layouts/${encodeURIComponent(layoutKey)}`, {
    method: "PUT",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ layout })
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось сохранить layout workflow: ${response.status}`));
  }

  return response.json() as Promise<{ layout: ScopeFlowLayout }>;
}

export async function fetchLatestSemanticAnalysis(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/analysis/semantic/latest`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить semantic analysis: ${response.status}`));
  }

  return response.json() as Promise<{ analysis: SemanticAnalysisRun | null }>;
}

export async function runSemanticAnalysis(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/analysis/semantic`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось выполнить semantic analysis: ${response.status}`));
  }

  return response.json() as Promise<{ analysis: SemanticAnalysisRun }>;
}

function getFilenameFromContentDisposition(value: string | null, fallback: string) {
  if (!value) {
    return fallback;
  }

  const match = /filename="?([^";]+)"?/i.exec(value);
  return match?.[1] ? decodeURIComponent(match[1]) : fallback;
}

export async function downloadSemanticAnalysisExport(projectId: string, format: SemanticExportFormat) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/analysis/semantic/export?format=${format}`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось выгрузить semantic export: ${response.status}`));
  }

  const extensions = {
    csv: "csv",
    json: "json",
    markdown: "md"
  } satisfies Record<SemanticExportFormat, string>;

  return {
    blob: await response.blob(),
    filename: getFilenameFromContentDisposition(
      response.headers.get("Content-Disposition"),
      `nodedc-seo-export.${extensions[format]}`
    )
  };
}

export async function fetchLatestMarketEnrichment(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/market-enrichment/latest`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить market enrichment: ${response.status}`));
  }

  return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
}

export async function runMarketEnrichment(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/market-enrichment`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось выполнить market enrichment: ${response.status}`));
  }

  return response.json() as Promise<{ marketEnrichment: MarketEnrichmentContract }>;
}

export async function fetchExternalServiceSettings() {
  const response = await fetch(`${apiBaseUrl}/settings/external-services`);

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось загрузить внешние сервисы: ${response.status}`));
  }

  return response.json() as Promise<{ externalServices: ExternalServiceSettings }>;
}

export async function updateExternalServiceSettings(
  serviceId: ExternalServiceSettings["services"][number]["id"],
  input: ExternalServiceUpdateInput
) {
  const response = await fetch(`${apiBaseUrl}/settings/external-services/${serviceId}`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(input)
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось сохранить внешний сервис: ${response.status}`));
  }

  return response.json() as Promise<{ service: ExternalServiceSettings["services"][number] }>;
}

export async function updateProjectPageSelection(projectId: string, pageIds: string[], selected: boolean) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/pages/selection`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ pageIds, selected })
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось обновить выбор страниц: ${response.status}`));
  }

  return response.json() as Promise<{ updatedCount: number; scan: ProjectScanSummary | null }>;
}

export async function openPageWorkspace(projectId: string, pageId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/pages/${pageId}/workspace`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось открыть рабочую зону: ${response.status}`));
  }

  return response.json() as Promise<{ workspace: PageWorkspace }>;
}

export async function savePageWorkspace(projectId: string, pageId: string, workingText: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/pages/${pageId}/workspace`, {
    method: "PATCH",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ workingText })
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось сохранить рабочий draft: ${response.status}`));
  }

  return response.json() as Promise<{ workspace: PageWorkspace }>;
}

export async function resetPageWorkspace(projectId: string, pageId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}/pages/${pageId}/workspace/reset`, {
    method: "POST"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось сбросить рабочий draft: ${response.status}`));
  }

  return response.json() as Promise<{ workspace: PageWorkspace }>;
}

export function getPreviewPageUrl(projectId: string, sourcePath: string, focusSelector?: string | null, focusIndex?: number | null) {
  const baseUrl = `${apiBaseUrl}/preview/projects/${encodeURIComponent(projectId)}/site/${encodePathname(sourcePath)}`;

  if (!focusSelector && focusIndex == null) {
    return baseUrl;
  }

  const params = new URLSearchParams();

  if (focusSelector) {
    params.set("focus", focusSelector);
  }

  if (focusIndex != null) {
    params.set("focusIndex", String(focusIndex));
  }

  return `${baseUrl}?${params.toString()}`;
}

export function getPreviewSnapshotUrl(
  projectId: string,
  cacheVersion: string,
  sourcePath: string,
  focusSelector?: string | null,
  focusIndex?: number | null
) {
  const baseUrl = `${apiBaseUrl}/preview/projects/${encodeURIComponent(projectId)}/snapshot/${encodePathname(sourcePath)}`;
  const params = new URLSearchParams({
    cacheVersion,
    viewportHeight: "1080",
    viewportWidth: "1920"
  });

  if (focusSelector) {
    params.set("focus", focusSelector);
  }

  if (focusIndex != null) {
    params.set("focusIndex", String(focusIndex));
  }

  return `${baseUrl}?${params.toString()}`;
}

export async function deleteProject(projectId: string) {
  const response = await fetch(`${apiBaseUrl}/projects/${projectId}`, {
    method: "DELETE"
  });

  if (!response.ok) {
    throw new Error(await getErrorMessage(response, `Не удалось удалить проект: ${response.status}`));
  }
}
