import {
  projectDirectoryPath,
  projectFilePath,
  projectParentDirectory,
  type ProjectDirectoryPath,
  type ProjectFilePath,
} from './project-path';
import {
  createDefaultLearnitiveProjectConfig,
  type LearnitiveProjectConfig,
} from './project-config';
import type { ProjectStorageProvider } from './types';

export type ProjectTemplateTier = 'free' | 'pro';
export type ProjectTemplateStatus = 'published';
export type ProjectTemplateProjectType = 'web' | 'code' | 'document';
export type ProjectTemplateValidationStatus = 'draft' | 'validating' | 'valid' | 'invalid' | 'needs-revalidation';

export type ProjectTemplateCategory = {
  id: string;
  slug: string;
  name: string;
  description?: string;
  icon: string;
  sortOrder: number;
  provenance?: 'official' | 'adapted' | 'community' | 'learnitive';
};

export type ProjectTemplateInstitution = {
  id: string;
  slug: string;
  name: string;
  displayName: string;
  shortName?: string;
  country?: string;
  countryCode?: string;
  region?: string;
  website?: string;
  logoUrl?: string;
  description?: string;
  seoDescription?: string;
  aliases: readonly string[];
};

export type ProjectTemplateFile = {
  path: ProjectFilePath;
  url: string;
  size: number;
};

export type ProjectTemplateDefinition = {
  id: string;
  slug: string;
  name: string;
  description: string;
  projectModeId: string;
  projectType: ProjectTemplateProjectType;
  primaryLanguageId: string;
  primaryLanguage: string;
  frameworkId?: string;
  framework?: string;
  useCaseId: string;
  buildOutput?: string;
  sourceType?: string;
  validationStatus: ProjectTemplateValidationStatus;
  validationRevision?: string;
  validationTimestamp?: string;
  validationError?: string;
  validationWarnings: readonly string[];
  validationRuntimeVersion?: string;
  category: string;
  categoryId?: string;
  author: string;
  originalAuthor?: string;
  tier: ProjectTemplateTier;
  status?: ProjectTemplateStatus;
  noIndex?: boolean;
  shortTitle?: string;
  longDescription?: string;
  institution?: string;
  country?: string;
  countryCode?: string;
  region?: string;
  degreeLevels: readonly string[];
  faculty?: string;
  department?: string;
  journal?: string;
  publisher?: string;
  conference?: string;
  provenance?: 'official' | 'adapted' | 'community' | 'learnitive';
  sourceUrl?: string;
  license?: string;
  attribution?: string;
  compiler?: 'pdflatex' | 'xelatex' | 'lualatex';
  bibliographyEngine?: string;
  documentClass?: string;
  requiredPackages: readonly string[];
  features: readonly string[];
  seoTitle?: string;
  seoDescription?: string;
  lastVerified?: string;
  tags: readonly string[];
  updatedAt?: string;
  entryFile: ProjectFilePath;
  templateVersion: string;
  thumbnail?: string;
  preview?: string;
  previewKind?: 'image' | 'pdf';
  totalBytes: number;
  files: readonly ProjectTemplateFile[];
  config?: LearnitiveProjectConfig;
};

export type ProjectTemplateManifest = {
  version: 1 | 2;
  generatedAt: string;
  contentHash: string;
  templates: readonly ProjectTemplateDefinition[];
};

const TEMPLATE_MANIFEST_URL = '/learnitive-templates/templates.json';
const TEMPLATE_API_PREFIX = '/api/templates/';
const TEMPLATE_ID_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/u;

function record(value: unknown): Record<string, unknown> | undefined {
  return value && typeof value === 'object' && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : undefined;
}

function hasControlCharacters(value: string): boolean {
  for (const character of value) {
    const codePoint = character.codePointAt(0);
    if (codePoint !== undefined && codePoint <= 0x1f) return true;
  }
  return false;
}

function requiredText(value: unknown, field: string, max = 1000): string {
  if (typeof value !== 'string') throw new Error(`Invalid template manifest ${field}.`);
  const result = value.trim();
  if (!result || result.length > max || hasControlCharacters(result)) {
    throw new Error(`Invalid template manifest ${field}.`);
  }
  return result;
}

function optionalAssetUrl(value: unknown, field: string, slug: string): string | undefined {
  if (value === null || value === undefined || value === '') return undefined;
  const result = requiredText(value, field, 2048);
  if (!result.startsWith(`${TEMPLATE_API_PREFIX}${slug}/assets/`) || result.includes('\\')) {
    throw new Error(`Invalid template manifest ${field}.`);
  }
  return result;
}

function optionalText(value: unknown, field: string, max = 1000): string | undefined {
  if (value === null || value === undefined || value === '') return undefined;
  return requiredText(value, field, max);
}

function optionalDiagnosticText(value: unknown, field: string, max = 4000): string | undefined {
  if (value === null || value === undefined || value === '') return undefined;
  if (typeof value !== 'string') throw new Error(`Invalid template manifest ${field}.`);
  const result = value
    .replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, '')
    .replace(/\r\n?/gu, '\n')
    .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '')
    .trim();
  if (!result || result.length > max) throw new Error(`Invalid template manifest ${field}.`);
  return result;
}

function textList(value: unknown, field: string, maxItems = 100): string[] {
  if (value === null || value === undefined) return [];
  if (!Array.isArray(value) || value.length > maxItems) throw new Error(`Invalid template manifest ${field}.`);
  return value.map((item) => requiredText(item, field, 240));
}

function parseDefinition(value: unknown, manifestVersion: 1 | 2): ProjectTemplateDefinition {
  const item = record(value);
  if (!item) throw new Error('Invalid template manifest item.');
  const id = requiredText(item.id, 'id', 80);
  const slug = requiredText(item.slug, 'slug', 80);
  if (id !== slug || !TEMPLATE_ID_RE.test(id)) throw new Error('Invalid template manifest id.');
  const tier = item.tier;
  if (tier !== 'free' && tier !== 'pro') throw new Error('Invalid template manifest tier.');
  const entryFile = projectFilePath(requiredText(item.entryFile, 'entryFile', 500));
  if (!Array.isArray(item.files) || item.files.length === 0) throw new Error('Invalid template manifest files.');
  const seen = new Set<string>();
  const files = item.files.map((rawFile) => {
    const candidate = record(rawFile);
    if (!candidate) throw new Error('Invalid template manifest file.');
    const path = projectFilePath(requiredText(candidate.path, 'file path', 500));
    if (seen.has(path)) throw new Error(`Duplicate template file path: ${path}`);
    seen.add(path);
    const url = requiredText(candidate.url, 'file url', 2048);
    if (!url.startsWith(`${TEMPLATE_API_PREFIX}${slug}/files/`) || url.includes('\\')) {
      throw new Error(`Invalid template file URL for ${path}.`);
    }
    const size = Number(candidate.size);
    if (!Number.isSafeInteger(size) || size < 0 || size > 25 * 1024 * 1024) {
      throw new Error(`Invalid template file size for ${path}.`);
    }
    return { path, url, size } satisfies ProjectTemplateFile;
  });
  if (!seen.has(entryFile)) throw new Error(`Template entry file is missing: ${entryFile}`);
  const rawTags = item.tags ?? [];
  if (!Array.isArray(rawTags) || rawTags.length > 30) throw new Error('Invalid template manifest tags.');
  const tags = rawTags.map((tag) => requiredText(tag, 'tag', 80));
  const projectModeId = manifestVersion === 1 ? 'latex' : requiredText(item.projectModeId, 'projectModeId', 80);
  const projectTypeRaw = manifestVersion === 1 ? 'document' : requiredText(item.projectType, 'projectType', 20);
  if (!['web','code','document'].includes(projectTypeRaw)) throw new Error('Invalid template manifest projectType.');
  const projectType = projectTypeRaw as ProjectTemplateProjectType;
  const primaryLanguageId = manifestVersion === 1 ? 'latex' : requiredText(item.primaryLanguageId, 'primaryLanguageId', 80);
  const primaryLanguage = manifestVersion === 1 ? 'LaTeX' : requiredText(item.primaryLanguage, 'primaryLanguage', 100);
  const validationStatusRaw = manifestVersion === 1 ? 'needs-revalidation' : requiredText(item.validationStatus, 'validationStatus', 40);
  if (!['draft','validating','valid','invalid','needs-revalidation'].includes(validationStatusRaw)) throw new Error('Invalid template manifest validationStatus.');
  const validationStatus = validationStatusRaw as ProjectTemplateValidationStatus;
  const config = projectModeId === 'latex' ? createDefaultLearnitiveProjectConfig() : undefined;
  if (config) config.rootFile = entryFile;
  return {
    id,
    slug,
    name: requiredText(item.name, 'name', 160),
    description: typeof item.description === 'string' ? item.description.trim().slice(0, 1000) : '',
    projectModeId,
    projectType,
    primaryLanguageId,
    primaryLanguage,
    frameworkId: optionalText(item.frameworkId, 'frameworkId', 80),
    framework: optionalText(item.framework, 'framework', 100),
    useCaseId: manifestVersion === 1 ? (optionalText(item.categoryId, 'categoryId', 100) || 'starter') : requiredText(item.useCaseId, 'useCaseId', 100),
    buildOutput: optionalText(item.buildOutput, 'buildOutput', 500),
    sourceType: optionalText(item.sourceType, 'sourceType', 40),
    validationStatus,
    validationRevision: optionalText(item.validationRevision, 'validationRevision', 128),
    validationTimestamp: optionalText(item.validationTimestamp, 'validationTimestamp', 64),
    validationError: optionalDiagnosticText(item.validationError, 'validationError', 4000),
    validationWarnings: Array.isArray(item.validationWarnings) ? item.validationWarnings.slice(0, 40).map((value) => optionalDiagnosticText(value, 'validationWarning', 2000)).filter((value): value is string => Boolean(value)) : [],
    validationRuntimeVersion: optionalText(item.validationRuntimeVersion, 'validationRuntimeVersion', 160),
    category: optionalText(item.category, 'category', 100) || '',
    categoryId: optionalText(item.categoryId, 'categoryId', 100),
    author: requiredText(item.author, 'author', 160),
    originalAuthor: optionalText(item.originalAuthor, 'originalAuthor', 160),
    tier,
    status: item.status === 'published' ? 'published' : undefined,
    noIndex: item.noIndex === true,
    shortTitle: optionalText(item.shortTitle, 'shortTitle', 100),
    longDescription: optionalText(item.longDescription, 'longDescription', 12000),
    institution: optionalText(item.institution, 'institution', 100),
    country: optionalText(item.country, 'country', 100),
    countryCode: optionalText(item.countryCode, 'countryCode', 3),
    region: optionalText(item.region, 'region', 120),
    degreeLevels: textList(item.degreeLevels, 'degreeLevels', 30),
    faculty: optionalText(item.faculty, 'faculty', 160),
    department: optionalText(item.department, 'department', 160),
    journal: optionalText(item.journal, 'journal', 180),
    publisher: optionalText(item.publisher, 'publisher', 180),
    conference: optionalText(item.conference, 'conference', 180),
    provenance: ['official','adapted','community','learnitive'].includes(String(item.provenance || '')) ? item.provenance as ProjectTemplateDefinition['provenance'] : undefined,
    sourceUrl: optionalText(item.sourceUrl, 'sourceUrl', 2048),
    license: optionalText(item.license, 'license', 240),
    attribution: optionalText(item.attribution, 'attribution', 2000),
    compiler: ['pdflatex','xelatex','lualatex'].includes(String(item.compiler || '')) ? item.compiler as ProjectTemplateDefinition['compiler'] : undefined,
    bibliographyEngine: optionalText(item.bibliographyEngine, 'bibliographyEngine', 32),
    documentClass: optionalText(item.documentClass, 'documentClass', 120),
    requiredPackages: textList(item.requiredPackages, 'requiredPackages'),
    features: textList(item.features, 'features'),
    seoTitle: optionalText(item.seoTitle, 'seoTitle', 180),
    seoDescription: optionalText(item.seoDescription, 'seoDescription', 320),
    lastVerified: optionalText(item.lastVerified, 'lastVerified', 64),
    tags,
    updatedAt: typeof item.updatedAt === 'string' && item.updatedAt.trim() ? item.updatedAt.trim().slice(0, 64) : undefined,
    entryFile,
    templateVersion: requiredText(item.templateVersion, 'templateVersion', 64),
    thumbnail: optionalAssetUrl(item.thumbnail, 'thumbnail', slug),
    preview: optionalAssetUrl(item.preview, 'preview', slug),
    previewKind: item.previewKind === 'image' || item.previewKind === 'pdf' ? item.previewKind : undefined,
    totalBytes: files.reduce((sum, file) => sum + file.size, 0),
    files,
    config,
  };
}

export async function loadProjectTemplateManifest(
  signal?: AbortSignal,
  fetcher: typeof fetch = fetch,
): Promise<ProjectTemplateManifest> {
  const response = await fetcher(TEMPLATE_MANIFEST_URL, {
    method: 'GET',
    cache: 'no-store',
    credentials: 'same-origin',
    signal,
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) throw new Error(`Template manifest request failed (${response.status}).`);
  const raw: unknown = await response.json();
  const manifest = record(raw);
  if (!manifest || (manifest.version !== 1 && manifest.version !== 2) || !Array.isArray(manifest.templates)) {
    throw new Error('Unsupported or invalid template manifest.');
  }
  const version = manifest.version as 1 | 2;
  const templates = manifest.templates.map((item) => parseDefinition(item, version));
  const ids = new Set<string>();
  for (const template of templates) {
    if (ids.has(template.id)) throw new Error(`Duplicate template manifest id: ${template.id}`);
    ids.add(template.id);
  }
  return {
    version,
    generatedAt: requiredText(manifest.generatedAt, 'generatedAt', 80),
    contentHash: requiredText(manifest.contentHash, 'contentHash', 128),
    templates,
  };
}

export async function loadProjectTemplateCategories(
  signal?: AbortSignal,
  fetcher: typeof fetch = fetch,
): Promise<readonly ProjectTemplateCategory[]> {
  const response = await fetcher('/api/templates/categories', {
    method: 'GET', cache: 'no-store', credentials: 'same-origin', signal, headers: { Accept: 'application/json' },
  });
  if (!response.ok) throw new Error(`Template categories request failed (${response.status}).`);
  const raw: unknown = await response.json();
  const payload = record(raw);
  if (!payload || !Array.isArray(payload.categories)) throw new Error('Invalid template categories response.');
  const seen = new Set<string>();
  return payload.categories.map((value) => {
    const item = record(value);
    if (!item) throw new Error('Invalid template category.');
    const id = requiredText(item.id, 'category id', 100);
    if (!TEMPLATE_ID_RE.test(id) || seen.has(id)) throw new Error('Invalid template category id.');
    seen.add(id);
    const sortOrder = Number(item.sortOrder ?? 0);
    if (!Number.isSafeInteger(sortOrder)) throw new Error('Invalid template category sort order.');
    return {
      id,
      slug: requiredText(item.slug ?? id, 'category slug', 100),
      name: requiredText(item.name, 'category name', 160),
      description: optionalText(item.description, 'category description', 1000),
      icon: optionalText(item.icon, 'category icon', 80) || 'FileText',
      sortOrder,
      provenance: ['official','adapted','community','learnitive'].includes(String(item.provenance || '')) ? item.provenance as ProjectTemplateCategory['provenance'] : undefined,
    } satisfies ProjectTemplateCategory;
  }).sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
}

export async function loadProjectTemplateInstitutions(
  signal?: AbortSignal,
  fetcher: typeof fetch = fetch,
): Promise<readonly ProjectTemplateInstitution[]> {
  const response = await fetcher('/api/templates/institutions', {
    method: 'GET', cache: 'no-store', credentials: 'same-origin', signal, headers: { Accept: 'application/json' },
  });
  if (!response.ok) throw new Error(`Template institutions request failed (${response.status}).`);
  const raw: unknown = await response.json();
  const payload = record(raw);
  if (!payload || !Array.isArray(payload.institutions)) throw new Error('Invalid template institutions response.');
  return payload.institutions.map((value) => {
    const item = record(value);
    if (!item) throw new Error('Invalid template institution.');
    const id = requiredText(item.id, 'institution id', 100);
    return {
      id,
      slug: requiredText(item.slug ?? id, 'institution slug', 100),
      name: requiredText(item.name, 'institution name', 180),
      displayName: requiredText(item.displayName ?? item.name, 'institution displayName', 180),
      shortName: optionalText(item.shortName, 'institution shortName', 40),
      country: optionalText(item.country, 'institution country', 100),
      countryCode: optionalText(item.countryCode, 'institution countryCode', 3),
      region: optionalText(item.region, 'institution region', 140),
      website: optionalText(item.website, 'institution website', 2048),
      logoUrl: item.logo ? `/api/templates/institutions/${encodeURIComponent(id)}/logo` : undefined,
      description: optionalText(item.description, 'institution description', 2000),
      seoDescription: optionalText(item.seoDescription, 'institution seoDescription', 320),
      aliases: textList(item.aliases, 'institution aliases', 40),
    } satisfies ProjectTemplateInstitution;
  });
}

export type PreparedProjectTemplateFile = {
  path: ProjectFilePath;
  bytes: Uint8Array;
};

/**
 * Fetch and validate the complete template before a workspace is created.
 * HTTP Content-Length is intentionally not compared with the manifest size:
 * browsers may expose the transfer/compressed length while arrayBuffer()
 * returns decoded bytes. The decoded byte length is the authoritative check.
 */
export async function fetchProjectTemplateFiles(
  template: ProjectTemplateDefinition,
  signal?: AbortSignal,
  fetcher: typeof fetch = fetch,
): Promise<readonly PreparedProjectTemplateFile[]> {
  const prepared: PreparedProjectTemplateFile[] = [];
  for (const templateFile of template.files) {
    signal?.throwIfAborted();
    const response = await fetcher(templateFile.url, {
      method: 'GET',
      cache: 'no-store',
      credentials: 'same-origin',
      signal,
    });
    if (!response.ok) throw new Error(`Template file request failed (${response.status}): ${templateFile.path}`);
    const bytes = new Uint8Array(await response.arrayBuffer());
    if (bytes.byteLength !== templateFile.size) {
      throw new Error(`Template catalog changed for ${templateFile.path}. Refresh the template library and try again. No project was created.`);
    }
    prepared.push({ path: templateFile.path, bytes });
  }
  return prepared;
}

export async function writePreparedProjectTemplateFiles(
  provider: ProjectStorageProvider,
  template: ProjectTemplateDefinition,
  prepared: readonly PreparedProjectTemplateFile[],
  signal?: AbortSignal,
): Promise<readonly ProjectFilePath[]> {
  signal?.throwIfAborted();
  const existing = await provider.listEntries(signal);
  if (existing.length > 0) {
    throw new Error('Project templates can only be created in an empty workspace.');
  }
  if (prepared.length !== template.files.length) throw new Error('Template project payload is incomplete.');
  const expected = new Map(template.files.map((file) => [file.path, file.size]));
  for (const file of prepared) {
    const size = expected.get(file.path);
    if (size === undefined || file.bytes.byteLength !== size) throw new Error(`Template project payload is invalid: ${file.path}`);
  }

  const written: ProjectFilePath[] = [];
  try {
    for (const file of prepared) {
      signal?.throwIfAborted();
      await provider.writeFile(file.path, file.bytes, signal);
      written.push(file.path);
    }
    return written;
  } catch (error) {
    for (const path of [...written].reverse()) {
      await provider.deleteFile(path).catch(() => undefined);
    }
    const directories = new Set<ProjectDirectoryPath>();
    for (const file of template.files) {
      let parent = projectParentDirectory(file.path);
      while (parent) {
        directories.add(parent);
        const withoutSlash = parent.slice(0, -1);
        const slash = withoutSlash.lastIndexOf('/');
        parent = slash < 0 ? undefined : projectDirectoryPath(withoutSlash.slice(0, slash + 1));
      }
    }
    for (const directory of [...directories].sort((a, b) => b.length - a.length)) {
      await provider.deleteDirectory(directory).catch(() => undefined);
    }
    throw error;
  }
}

export async function instantiateProjectTemplate(
  provider: ProjectStorageProvider,
  template: ProjectTemplateDefinition,
  signal?: AbortSignal,
  fetcher: typeof fetch = fetch,
): Promise<readonly ProjectFilePath[]> {
  const prepared = await fetchProjectTemplateFiles(template, signal, fetcher);
  return writePreparedProjectTemplateFiles(provider, template, prepared, signal);
}
