import type { PoolClient } from "pg";
import crypto from "node:crypto";
import path from "node:path";
import { pool } from "../db/client.js";
import { storageProvider } from "../storage/index.js";
import { validateLocalFolderPath } from "./localFolder.js";

type ProjectRow = {
  id: string;
  name: string;
  slug: string;
  created_at: Date;
  updated_at: Date;
  source_id: string | null;
  source_type: string | null;
  source_config: Record<string, unknown> | null;
  source_created_at: Date | null;
  source_updated_at: Date | null;
};

type ProjectRecord = {
  id: string;
  name: string;
  slug: string;
  created_at: Date;
  updated_at: Date;
};

type SourceRecord = {
  id: string;
  type: string;
  config: Record<string, unknown>;
  created_at: Date;
  updated_at: Date;
};

type SourceConfigRow = {
  config: Record<string, unknown>;
};

type ProjectSourceCopyRow = {
  id: string;
  name: string;
  source_id: string;
  source_type: string;
  source_config: Record<string, unknown>;
};

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 CreateProjectInput = {
  name?: string;
  sourcePath: string;
};

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

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

export class ProjectNameConflictError extends Error {
  constructor(name: string) {
    super(`Проект с названием «${name}» уже есть.`);
    this.name = "ProjectNameConflictError";
  }
}

export class ProjectCopyError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ProjectCopyError";
  }
}

const CYRILLIC_TO_LATIN: Record<string, string> = {
  а: "a",
  б: "b",
  в: "v",
  г: "g",
  д: "d",
  е: "e",
  ё: "e",
  ж: "zh",
  з: "z",
  и: "i",
  й: "y",
  к: "k",
  л: "l",
  м: "m",
  н: "n",
  о: "o",
  п: "p",
  р: "r",
  с: "s",
  т: "t",
  у: "u",
  ф: "f",
  х: "h",
  ц: "ts",
  ч: "ch",
  ш: "sh",
  щ: "sch",
  ъ: "",
  ы: "y",
  ь: "",
  э: "e",
  ю: "yu",
  я: "ya"
};

const MAX_PROJECT_NAME_LENGTH = 120;

function toIsoDate(value: Date) {
  return value.toISOString();
}

function getSourcePath(config: Record<string, unknown>) {
  return typeof config.path === "string" ? config.path : null;
}

function sanitizeObjectPath(value: string) {
  const normalizedPath = value.replace(/\\/g, "/").replace(/^\/+/, "");
  const segments = normalizedPath.split("/").filter(Boolean);

  if (segments.length === 0) {
    throw new Error("Файл без относительного пути нельзя импортировать.");
  }

  if (segments.some((segment) => segment === "." || segment === "..")) {
    throw new Error("В импорте найден небезопасный путь файла.");
  }

  return segments.join("/");
}

function mapProjectRow(row: ProjectRow): ProjectSummary {
  const sourceConfig = row.source_config ?? {};

  return {
    id: row.id,
    name: row.name,
    slug: row.slug,
    createdAt: toIsoDate(row.created_at),
    updatedAt: toIsoDate(row.updated_at),
    source:
      row.source_id && row.source_type && row.source_created_at && row.source_updated_at
        ? {
            id: row.source_id,
            type: row.source_type,
            path: getSourcePath(sourceConfig),
            config: sourceConfig,
            createdAt: toIsoDate(row.source_created_at),
            updatedAt: toIsoDate(row.source_updated_at)
          }
        : null
  };
}

function mapProjectRecord(project: ProjectRecord, source: SourceRecord): ProjectSummary {
  return {
    id: project.id,
    name: project.name,
    slug: project.slug,
    createdAt: toIsoDate(project.created_at),
    updatedAt: toIsoDate(project.updated_at),
    source: {
      id: source.id,
      type: source.type,
      path: getSourcePath(source.config),
      config: source.config,
      createdAt: toIsoDate(source.created_at),
      updatedAt: toIsoDate(source.updated_at)
    }
  };
}

async function selectProjectById(client: PoolClient, projectId: string) {
  const result = await client.query<ProjectRow>(
    `
      select
        p.id,
        p.name,
        p.slug,
        p.created_at,
        p.updated_at,
        ps.id as source_id,
        ps.type as source_type,
        ps.config as source_config,
        ps.created_at as source_created_at,
        ps.updated_at as source_updated_at
      from projects p
      left join lateral (
        select id, type, config, created_at, updated_at
        from project_sources
        where project_id = p.id
        order by created_at asc
        limit 1
      ) ps on true
      where p.id = $1;
    `,
    [projectId]
  );

  return result.rows[0] ? mapProjectRow(result.rows[0]) : null;
}

async function selectProjectSourceForCopy(client: PoolClient, projectId: string) {
  const result = await client.query<ProjectSourceCopyRow>(
    `
      select
        p.id,
        p.name,
        ps.id as source_id,
        ps.type as source_type,
        ps.config as source_config
      from projects p
      join lateral (
        select id, type, config
        from project_sources
        where project_id = p.id
        order by created_at asc
        limit 1
      ) ps on true
      where p.id = $1;
    `,
    [projectId]
  );

  return result.rows[0] ?? null;
}

async function insertProjectWithSource(
  client: PoolClient,
  input: {
    name: string;
    sourceType: "local_folder" | "browser_folder";
    sourceConfig: Record<string, unknown>;
  }
) {
  const slug = await buildUniqueSlug(client, input.name);
  const projectResult = await client.query<ProjectRecord>(
    `
      insert into projects (name, slug)
      values ($1, $2)
      returning id, name, slug, created_at, updated_at;
    `,
    [input.name, slug]
  );
  const project = projectResult.rows[0];

  if (!project) {
    throw new Error("Не удалось создать проект.");
  }

  const sourceResult = await client.query<SourceRecord>(
    `
      insert into project_sources (project_id, type, config)
      values ($1, $2, $3)
      returning id, type, config, created_at, updated_at;
    `,
    [project.id, input.sourceType, input.sourceConfig]
  );
  const source = sourceResult.rows[0];

  if (!source) {
    throw new Error("Не удалось сохранить источник проекта.");
  }

  return { project, source };
}

function transliterate(value: string) {
  return value
    .toLowerCase()
    .split("")
    .map((char) => CYRILLIC_TO_LATIN[char] ?? char)
    .join("");
}

function slugifyName(name: string) {
  const slug = transliterate(name)
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 64);

  return slug || "project";
}

async function slugExists(client: PoolClient, slug: string, excludeProjectId?: string) {
  const result = excludeProjectId
    ? await client.query<{ exists: boolean }>(
        "select exists(select 1 from projects where slug = $1 and id <> $2) as exists",
        [slug, excludeProjectId]
      )
    : await client.query<{ exists: boolean }>(
        "select exists(select 1 from projects where slug = $1) as exists",
        [slug]
      );

  return result.rows[0]?.exists ?? false;
}

async function projectNameExists(client: PoolClient, name: string, excludeProjectId?: string) {
  const result = excludeProjectId
    ? await client.query<{ exists: boolean }>(
        "select exists(select 1 from projects where lower(name) = lower($1) and id <> $2) as exists",
        [name, excludeProjectId]
      )
    : await client.query<{ exists: boolean }>(
        "select exists(select 1 from projects where lower(name) = lower($1)) as exists",
        [name]
      );

  return result.rows[0]?.exists ?? false;
}

async function assertProjectNameAvailable(client: PoolClient, name: string, excludeProjectId?: string) {
  if (await projectNameExists(client, name, excludeProjectId)) {
    throw new ProjectNameConflictError(name);
  }
}

async function buildUniqueSlug(client: PoolClient, name: string, excludeProjectId?: string) {
  const baseSlug = slugifyName(name);
  let suffix = 1;

  while (suffix < 1000) {
    const suffixText = suffix === 1 ? "" : `-${suffix}`;
    const candidate = `${baseSlug.slice(0, 64 - suffixText.length)}${suffixText}`;

    if (!(await slugExists(client, candidate, excludeProjectId))) {
      return candidate;
    }

    suffix += 1;
  }

  return `${baseSlug.slice(0, 50)}-${Date.now()}`;
}

function getCopyBaseName(originalName: string) {
  let baseName = originalName.trim() || "project";
  let previousBaseName = "";

  while (baseName && baseName !== previousBaseName) {
    previousBaseName = baseName;
    baseName = baseName
      .replace(/^(?:(?:\d{2}|[1-9]\d{0,2})\s*-\s*копия\s*-\s*|Копия(?:\s+\d+)?\s*-\s*|[1-9]\d{0,2}\s*-\s*)/iu, "")
      .trim();
  }

  return baseName || "project";
}

function buildCopyNameCandidate(originalName: string, copyIndex: number) {
  const prefix = `${String(copyIndex).padStart(2, "0")} - копия - `;
  const normalizedOriginalName = getCopyBaseName(originalName);
  const maxOriginalNameLength = Math.max(1, MAX_PROJECT_NAME_LENGTH - prefix.length);

  return `${prefix}${normalizedOriginalName.slice(0, maxOriginalNameLength)}`;
}

async function buildUniqueCopyName(client: PoolClient, originalName: string) {
  let copyIndex = 1;

  while (copyIndex < 1000) {
    const candidate = buildCopyNameCandidate(originalName, copyIndex);

    if (!(await projectNameExists(client, candidate))) {
      return candidate;
    }

    copyIndex += 1;
  }

  return buildCopyNameCandidate(`${crypto.randomUUID().slice(0, 8)}-${originalName}`, 1);
}

export async function listProjects() {
  const result = await pool.query<ProjectRow>(`
    select
      p.id,
      p.name,
      p.slug,
      p.created_at,
      p.updated_at,
      ps.id as source_id,
      ps.type as source_type,
      ps.config as source_config,
      ps.created_at as source_created_at,
      ps.updated_at as source_updated_at
    from projects p
    left join lateral (
      select id, type, config, created_at, updated_at
      from project_sources
      where project_id = p.id
      order by created_at asc
      limit 1
    ) ps on true
    order by p.created_at desc;
  `);

  return result.rows.map(mapProjectRow);
}

export async function createProject(input: CreateProjectInput) {
  const resolvedSourcePath = await validateLocalFolderPath(input.sourcePath);
  const name = input.name?.trim() || path.basename(resolvedSourcePath) || "project";
  const client = await pool.connect();

  try {
    await client.query("begin");
    await assertProjectNameAvailable(client, name);
    const { project, source } = await insertProjectWithSource(client, {
      name,
      sourceType: "local_folder",
      sourceConfig: {
          path: resolvedSourcePath,
          originalPath: input.sourcePath.trim()
      }
    });

    await client.query("commit");
    return mapProjectRecord(project, source);
  } catch (error) {
    await client.query("rollback");
    throw error;
  } finally {
    client.release();
  }
}

export async function createBrowserFolderProject(input: CreateBrowserFolderProjectInput) {
  const rootName = input.rootName.trim() || "browser-folder";
  const name = input.name?.trim() || rootName;
  const files = input.files.map((file) => ({
    ...file,
    relativePath: sanitizeObjectPath(file.relativePath)
  }));
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
  const client = await pool.connect();

  if (files.length === 0) {
    throw new Error("В выбранной папке нет файлов для импорта.");
  }

  try {
    await client.query("begin");
    await assertProjectNameAvailable(client, name);

    const importPrefixSeed = crypto.randomUUID();
    const importPrefix = `browser-folder-imports/${importPrefixSeed}`;
    const { project, source } = await insertProjectWithSource(client, {
      name,
      sourceType: "browser_folder",
      sourceConfig: {
        rootName,
        importPrefix,
        fileCount: files.length,
        totalBytes,
        importedAt: new Date().toISOString(),
        sampleFiles: files.slice(0, 20).map((file) => file.relativePath)
      }
    });

    await storageProvider.ensureReady();

    for (const file of files) {
      await storageProvider.putObject({
        key: `${importPrefix}/${file.relativePath}`,
        body: Buffer.from(file.contentBase64, "base64"),
        contentType: file.contentType || "application/octet-stream"
      });
    }

    await storageProvider.putObject({
      key: `${importPrefix}/manifest.json`,
      body: JSON.stringify(
        {
          projectId: project.id,
          sourceId: source.id,
          rootName,
          fileCount: files.length,
          totalBytes,
          files: files.map((file) => ({
            relativePath: file.relativePath,
            contentType: file.contentType,
            size: file.size,
            lastModified: file.lastModified
          }))
        },
        null,
        2
      ),
      contentType: "application/json"
    });

    await client.query("commit");
    return mapProjectRecord(project, source);
  } catch (error) {
    await client.query("rollback");
    throw error;
  } finally {
    client.release();
  }
}

export async function updateProjectName(projectId: string, name: string) {
  const nextName = name.trim();
  const client = await pool.connect();

  if (!nextName) {
    throw new Error("Название проекта обязательно.");
  }

  try {
    await client.query("begin");
    await assertProjectNameAvailable(client, nextName, projectId);

    const nextSlug = await buildUniqueSlug(client, nextName, projectId);
    const updateResult = await client.query(
      `
        update projects
        set name = $1, slug = $2, updated_at = now()
        where id = $3
        returning id;
      `,
      [nextName, nextSlug, projectId]
    );

    if (updateResult.rowCount === 0) {
      await client.query("rollback");
      return null;
    }

    const project = await selectProjectById(client, projectId);

    await client.query("commit");
    return project;
  } catch (error) {
    await client.query("rollback");
    throw error;
  } finally {
    client.release();
  }
}

export async function copyProject(projectId: string) {
  const client = await pool.connect();
  let copiedImportPrefix: string | null = null;

  try {
    await client.query("begin");

    const sourceProject = await selectProjectSourceForCopy(client, projectId);

    if (!sourceProject) {
      await client.query("rollback");
      return null;
    }

    const copiedAt = new Date().toISOString();
    const sourceConfig: Record<string, unknown> = {
      ...sourceProject.source_config,
      copiedFromProjectId: sourceProject.id,
      copiedFromSourceId: sourceProject.source_id,
      copiedAt
    };
    let sourceType: "local_folder" | "browser_folder";
    let sourceImportPrefix: string | null = null;

    if (sourceProject.source_type === "local_folder") {
      sourceType = "local_folder";
    } else if (sourceProject.source_type === "browser_folder") {
      const importPrefix = sourceProject.source_config.importPrefix;

      if (typeof importPrefix !== "string" || importPrefix.length === 0) {
        throw new ProjectCopyError("У исходного проекта нет корректного prefix импорта.");
      }

      sourceType = "browser_folder";
      sourceImportPrefix = importPrefix;
      copiedImportPrefix = `browser-folder-imports/${crypto.randomUUID()}`;
      sourceConfig.importPrefix = copiedImportPrefix;
      sourceConfig.copiedFromImportPrefix = sourceImportPrefix;
    } else {
      throw new ProjectCopyError("Копирование этого типа источника пока не поддерживается.");
    }

    const copyName = await buildUniqueCopyName(client, sourceProject.name);
    const { project, source } = await insertProjectWithSource(client, {
      name: copyName,
      sourceType,
      sourceConfig
    });

    if (sourceType === "browser_folder" && sourceImportPrefix && copiedImportPrefix) {
      const copiedObjects = await storageProvider.copyPrefix(sourceImportPrefix, copiedImportPrefix);

      if (copiedObjects === 0) {
        throw new ProjectCopyError("В хранилище не найдены файлы исходного проекта.");
      }

      await storageProvider.putObject({
        key: `${copiedImportPrefix}/manifest.json`,
        body: JSON.stringify(
          {
            projectId: project.id,
            sourceId: source.id,
            copiedFromProjectId: sourceProject.id,
            copiedFromSourceId: sourceProject.source_id,
            copiedFromImportPrefix: sourceImportPrefix,
            copiedAt,
            rootName: sourceConfig.rootName,
            fileCount: sourceConfig.fileCount,
            totalBytes: sourceConfig.totalBytes,
            sampleFiles: sourceConfig.sampleFiles
          },
          null,
          2
        ),
        contentType: "application/json"
      });
    }

    await client.query("commit");
    return mapProjectRecord(project, source);
  } catch (error) {
    await client.query("rollback");

    if (copiedImportPrefix) {
      try {
        await storageProvider.deletePrefix(copiedImportPrefix);
      } catch (cleanupError) {
        console.warn(`Не удалось подчистить файлы неудачной копии по prefix ${copiedImportPrefix}`, cleanupError);
      }
    }

    throw error;
  } finally {
    client.release();
  }
}

export async function deleteProject(projectId: string) {
  const client = await pool.connect();

  try {
    const sourceConfigs = await client.query<SourceConfigRow>(
      `
        select config
        from project_sources
        where project_id = $1 and type = 'browser_folder';
      `,
      [projectId]
    );
    const importPrefixes = sourceConfigs.rows
      .map((row) => row.config.importPrefix)
      .filter((value): value is string => typeof value === "string" && value.length > 0);

    await client.query("begin");

    const deleteResult = await client.query("delete from projects where id = $1", [projectId]);

    if (deleteResult.rowCount === 0) {
      await client.query("rollback");
      return false;
    }

    await client.query("commit");

    for (const prefix of importPrefixes) {
      try {
        await storageProvider.deletePrefix(prefix);
      } catch (error) {
        console.warn(`Не удалось удалить файлы проекта из хранилища по prefix ${prefix}`, error);
      }
    }

    return true;
  } catch (error) {
    await client.query("rollback");
    throw error;
  } finally {
    client.release();
  }
}
