import { and, asc, desc, eq, inArray, isNull } from 'drizzle-orm';
import {
  crmDocumentLibraries,
  crmDocumentLibraryVersions,
  users
} from '@/db/schema';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { auditAction } from '@/features/foundation/audit-log/service';
import {
  buildOrganizationStorageKey,
  getStorageProvider
} from '@/features/foundation/storage/service';
import type {
  DocumentLibraryDetail,
  DocumentLibraryFilters,
  DocumentLibraryListItem,
  DocumentLibraryMutationPayload,
  DocumentLibraryRecord,
  DocumentLibraryVersionRecord,
  DocumentLibraryVersionStatus
} from '../types';
import {
  assertCanDeleteDraftVersion,
  assertLifecycleTransition,
  assertSupportedBrand,
  assertSupportedDocumentType,
  assertSupportedLanguage,
  assertSupportedProductType,
  normalizeDocumentLibraryCode,
  parsePdfUpload
} from './validation';

function sanitizeFileName(fileName: string) {
  return fileName.replace(/[^a-zA-Z0-9._-]+/g, '-');
}

function mapLibrary(row: typeof crmDocumentLibraries.$inferSelect): DocumentLibraryRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    code: row.code,
    name: row.name,
    documentType: row.documentType as DocumentLibraryRecord['documentType'],
    description: row.description,
    brand: row.brand as DocumentLibraryRecord['brand'],
    language: row.language as DocumentLibraryRecord['language'],
    productType: row.productType as DocumentLibraryRecord['productType'],
    status: row.status as DocumentLibraryRecord['status'],
    isActive: row.isActive,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

function mapVersion(
  row: typeof crmDocumentLibraryVersions.$inferSelect
): DocumentLibraryVersionRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    libraryId: row.libraryId,
    version: row.version,
    fileName: row.fileName,
    storageProvider: row.storageProvider,
    storageKey: row.storageKey,
    mimeType: row.mimeType,
    fileSize: row.fileSize,
    checksum: row.checksum,
    pageCount: row.pageCount,
    status: row.status as DocumentLibraryVersionStatus,
    isActive: row.isActive,
    publishedAt: row.publishedAt?.toISOString() ?? null,
    publishedBy: row.publishedBy,
    archivedAt: row.archivedAt?.toISOString() ?? null,
    archivedBy: row.archivedBy,
    createdBy: row.createdBy,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null
  };
}

async function getLibraryRow(id: string, organizationId: string) {
  const [library] = await db
    .select()
    .from(crmDocumentLibraries)
    .where(
      and(
        eq(crmDocumentLibraries.id, id),
        eq(crmDocumentLibraries.organizationId, organizationId),
        isNull(crmDocumentLibraries.deletedAt)
      )
    )
    .limit(1);

  if (!library) {
    throw new AuthError('Document library item not found', 404);
  }

  return library;
}

async function getVersionRow(versionId: string, organizationId: string) {
  const [version] = await db
    .select()
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        eq(crmDocumentLibraryVersions.id, versionId),
        eq(crmDocumentLibraryVersions.organizationId, organizationId),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    )
    .limit(1);

  if (!version) {
    throw new AuthError('Document library version not found', 404);
  }

  return version;
}

async function ensureUniqueCode(
  organizationId: string,
  code: string,
  excludeId?: string
) {
  const rows = await db
    .select({ id: crmDocumentLibraries.id })
    .from(crmDocumentLibraries)
    .where(
      and(
        eq(crmDocumentLibraries.organizationId, organizationId),
        eq(crmDocumentLibraries.code, code),
        isNull(crmDocumentLibraries.deletedAt)
      )
    );

  if (rows.some((row) => row.id !== excludeId)) {
    throw new Error(`Document library code already exists: ${code}`);
  }
}

export async function listDocumentLibraries(
  organizationId: string,
  filters: DocumentLibraryFilters = {}
): Promise<DocumentLibraryListItem[]> {
  const libraries = await db
    .select()
    .from(crmDocumentLibraries)
    .where(
      and(
        eq(crmDocumentLibraries.organizationId, organizationId),
        isNull(crmDocumentLibraries.deletedAt),
        ...(filters.documentType
          ? [eq(crmDocumentLibraries.documentType, filters.documentType)]
          : []),
        ...(filters.brand ? [eq(crmDocumentLibraries.brand, filters.brand)] : []),
        ...(filters.language ? [eq(crmDocumentLibraries.language, filters.language)] : []),
        ...(filters.productType
          ? [eq(crmDocumentLibraries.productType, filters.productType)]
          : []),
        ...(filters.isActive === 'true' ? [eq(crmDocumentLibraries.isActive, true)] : []),
        ...(filters.isActive === 'false' ? [eq(crmDocumentLibraries.isActive, false)] : [])
      )
    )
    .orderBy(asc(crmDocumentLibraries.name));

  if (!libraries.length) {
    return [];
  }

  const versions = await db
    .select()
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        inArray(
          crmDocumentLibraryVersions.libraryId,
          libraries.map((library) => library.id)
        ),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    )
    .orderBy(desc(crmDocumentLibraryVersions.createdAt));

  return libraries.map((library) => {
    const libraryVersions = versions.filter((version) => version.libraryId === library.id);
    const activeVersion =
      libraryVersions.find((version) => version.isActive)?.version ?? null;

    return {
      ...mapLibrary(library),
      activeVersion,
      versionCount: libraryVersions.length
    };
  });
}

export async function getDocumentLibraryById(
  id: string,
  organizationId: string
): Promise<DocumentLibraryDetail> {
  const library = await getLibraryRow(id, organizationId);
  const versions = await db
    .select()
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        eq(crmDocumentLibraryVersions.libraryId, id),
        eq(crmDocumentLibraryVersions.organizationId, organizationId),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    )
    .orderBy(desc(crmDocumentLibraryVersions.createdAt));

  return {
    ...mapLibrary(library),
    versions: versions.map(mapVersion)
  };
}

export async function createDocumentLibrary(input: {
  organizationId: string;
  userId: string;
  values: DocumentLibraryMutationPayload;
}) {
  assertSupportedDocumentType(input.values.documentType);
  assertSupportedBrand(input.values.brand);
  assertSupportedLanguage(input.values.language);
  assertSupportedProductType(input.values.productType);

  const code = normalizeDocumentLibraryCode(input.values.code);
  await ensureUniqueCode(input.organizationId, code);

  const [created] = await db
    .insert(crmDocumentLibraries)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.organizationId,
      code,
      name: input.values.name.trim(),
      documentType: input.values.documentType,
      description: input.values.description?.trim() || null,
      brand: input.values.brand,
      language: input.values.language,
      productType: input.values.productType,
      status: input.values.isActive === false ? 'inactive' : 'active',
      isActive: input.values.isActive ?? true,
      createdBy: input.userId,
      updatedBy: input.userId
    })
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library',
    entityId: created.id,
    action: 'create',
    afterData: created
  });

  return mapLibrary(created);
}

export async function updateDocumentLibrary(input: {
  id: string;
  organizationId: string;
  userId: string;
  values: Partial<DocumentLibraryMutationPayload>;
}) {
  const current = await getLibraryRow(input.id, input.organizationId);

  if (input.values.documentType) {
    assertSupportedDocumentType(input.values.documentType);
  }

  if (input.values.brand) {
    assertSupportedBrand(input.values.brand);
  }

  if (input.values.language) {
    assertSupportedLanguage(input.values.language);
  }

  if (input.values.productType) {
    assertSupportedProductType(input.values.productType);
  }

  const code =
    typeof input.values.code === 'string'
      ? normalizeDocumentLibraryCode(input.values.code)
      : current.code;

  await ensureUniqueCode(input.organizationId, code, current.id);

  const [updated] = await db
    .update(crmDocumentLibraries)
    .set({
      code,
      name: input.values.name?.trim() ?? current.name,
      documentType: input.values.documentType ?? current.documentType,
      description:
        input.values.description !== undefined
          ? input.values.description?.trim() || null
          : current.description,
      brand: input.values.brand ?? current.brand,
      language: input.values.language ?? current.language,
      productType: input.values.productType ?? current.productType,
      status:
        input.values.isActive !== undefined
          ? input.values.isActive
            ? 'active'
            : 'inactive'
          : (current.status as 'active' | 'inactive'),
      isActive: input.values.isActive ?? current.isActive,
      updatedBy: input.userId,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentLibraries.id, current.id))
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library',
    entityId: updated.id,
    action: 'update',
    beforeData: current,
    afterData: updated
  });

  return mapLibrary(updated);
}

export async function uploadDocumentLibraryVersion(input: {
  libraryId: string;
  organizationId: string;
  userId: string;
  version: string;
  file: {
    name: string;
    type: string;
    size: number;
    buffer: Buffer;
  };
}) {
  const library = await getLibraryRow(input.libraryId, input.organizationId);
  const normalizedVersion = input.version.trim();

  if (!normalizedVersion) {
    throw new Error('Version is required.');
  }

  const existing = await db
    .select({ id: crmDocumentLibraryVersions.id })
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        eq(crmDocumentLibraryVersions.libraryId, library.id),
        eq(crmDocumentLibraryVersions.version, normalizedVersion),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    );

  if (existing.length > 0) {
    throw new Error(`Version already exists: ${normalizedVersion}`);
  }

  const parsedFile = await parsePdfUpload({
    fileName: input.file.name,
    mimeType: input.file.type,
    size: input.file.size,
    buffer: input.file.buffer
  });

  const versionId = crypto.randomUUID();
  const storageKey = buildOrganizationStorageKey(input.organizationId, [
    'document-library',
    library.id,
    versionId,
    sanitizeFileName(parsedFile.fileName)
  ]);

  const stored = await getStorageProvider().putObject({
    key: storageKey,
    body: input.file.buffer,
    contentType: parsedFile.contentType,
    fileName: parsedFile.fileName
  });

  const [created] = await db
    .insert(crmDocumentLibraryVersions)
    .values({
      id: versionId,
      organizationId: input.organizationId,
      libraryId: library.id,
      version: normalizedVersion,
      fileName: parsedFile.fileName,
      storageProvider: stored.provider,
      storageKey: stored.key,
      mimeType: parsedFile.contentType,
      fileSize: parsedFile.fileSize,
      checksum: parsedFile.checksum,
      pageCount: parsedFile.pageCount,
      status: 'draft',
      isActive: false,
      createdBy: input.userId
    })
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library_version',
    entityId: created.id,
    action: 'upload',
    afterData: created
  });

  return mapVersion(created);
}

async function setVersionStatus(input: {
  versionId: string;
  organizationId: string;
  userId: string;
  nextStatus: DocumentLibraryVersionStatus;
}) {
  const current = await getVersionRow(input.versionId, input.organizationId);

  if (input.nextStatus === 'archived' && current.isActive) {
    throw new Error('Active versions must be replaced before archiving.');
  }

  assertLifecycleTransition(
    current.status as DocumentLibraryVersionStatus,
    input.nextStatus
  );

  const [updated] = await db
    .update(crmDocumentLibraryVersions)
    .set({
      status: input.nextStatus,
      isActive: input.nextStatus === 'active',
      publishedAt:
        input.nextStatus === 'published' || input.nextStatus === 'active'
          ? current.publishedAt ?? new Date()
          : current.publishedAt,
      publishedBy:
        input.nextStatus === 'published' || input.nextStatus === 'active'
          ? current.publishedBy ?? input.userId
          : current.publishedBy,
      archivedAt: input.nextStatus === 'archived' ? new Date() : null,
      archivedBy: input.nextStatus === 'archived' ? input.userId : null,
      updatedAt: new Date()
    })
    .where(eq(crmDocumentLibraryVersions.id, current.id))
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library_version',
    entityId: updated.id,
    action: input.nextStatus,
    beforeData: current,
    afterData: updated
  });

  return mapVersion(updated);
}

export async function publishDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  return setVersionStatus({ ...input, nextStatus: 'published' });
}

export async function activateDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const current = await getVersionRow(input.versionId, input.organizationId);
  assertLifecycleTransition(current.status as DocumentLibraryVersionStatus, 'active');

  const [updated] = await db.transaction(async (tx) => {
    await tx
      .update(crmDocumentLibraryVersions)
      .set({
        isActive: false,
        updatedAt: new Date()
      })
      .where(
        and(
          eq(crmDocumentLibraryVersions.libraryId, current.libraryId),
          eq(crmDocumentLibraryVersions.organizationId, input.organizationId),
          isNull(crmDocumentLibraryVersions.deletedAt)
        )
      );

    const [next] = await tx
      .update(crmDocumentLibraryVersions)
      .set({
        status: 'active',
        isActive: true,
        publishedAt: current.publishedAt ?? new Date(),
        publishedBy: current.publishedBy ?? input.userId,
        updatedAt: new Date()
      })
      .where(eq(crmDocumentLibraryVersions.id, current.id))
      .returning();

    await tx
      .update(crmDocumentLibraries)
      .set({
        isActive: true,
        status: 'active',
        updatedAt: new Date(),
        updatedBy: input.userId
      })
      .where(eq(crmDocumentLibraries.id, current.libraryId));

    return [next] as const;
  });

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library_version',
    entityId: updated.id,
    action: 'activate',
    beforeData: current,
    afterData: updated
  });

  return mapVersion(updated);
}

export async function archiveDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  return setVersionStatus({ ...input, nextStatus: 'archived' });
}

export async function deleteDraftDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  const current = await getVersionRow(input.versionId, input.organizationId);
  assertCanDeleteDraftVersion(
    current.status as DocumentLibraryVersionStatus,
    current.isActive
  );

  await getStorageProvider().deleteObject({ key: current.storageKey });

  const [updated] = await db
    .update(crmDocumentLibraryVersions)
    .set({
      deletedAt: new Date(),
      updatedAt: new Date()
    })
    .where(eq(crmDocumentLibraryVersions.id, current.id))
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library_version',
    entityId: current.id,
    action: 'delete',
    beforeData: current,
    afterData: updated
  });

  return mapVersion(updated);
}

async function streamVersionFile(input: {
  versionId: string;
  organizationId: string;
  userId: string;
  action: 'preview' | 'download';
}) {
  const version = await getVersionRow(input.versionId, input.organizationId);
  const object = await getStorageProvider().getObject({ key: version.storageKey });

  await auditAction({
    organizationId: input.organizationId,
    userId: input.userId,
    entityType: 'crm_document_library_version',
    entityId: version.id,
    action: input.action,
    afterData: {
      storageKey: version.storageKey,
      fileName: version.fileName
    }
  });

  return {
    version: mapVersion(version),
    object: {
      ...object,
      fileName: version.fileName,
      contentType: version.mimeType,
      size: version.fileSize,
      checksum: version.checksum
    }
  };
}

export async function previewDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  return streamVersionFile({ ...input, action: 'preview' });
}

export async function downloadDocumentLibraryVersion(input: {
  versionId: string;
  organizationId: string;
  userId: string;
}) {
  return streamVersionFile({ ...input, action: 'download' });
}

export async function downloadActiveDocumentLibraryVersion(input: {
  libraryId: string;
  organizationId: string;
  userId: string;
}) {
  const [activeVersion] = await db
    .select()
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        eq(crmDocumentLibraryVersions.libraryId, input.libraryId),
        eq(crmDocumentLibraryVersions.organizationId, input.organizationId),
        eq(crmDocumentLibraryVersions.isActive, true),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    )
    .limit(1);

  if (!activeVersion) {
    throw new AuthError('No active document version found', 404);
  }

  return downloadDocumentLibraryVersion({
    versionId: activeVersion.id,
    organizationId: input.organizationId,
    userId: input.userId
  });
}

export async function previewActiveDocumentLibraryVersion(input: {
  libraryId: string;
  organizationId: string;
  userId: string;
}) {
  const [activeVersion] = await db
    .select()
    .from(crmDocumentLibraryVersions)
    .where(
      and(
        eq(crmDocumentLibraryVersions.libraryId, input.libraryId),
        eq(crmDocumentLibraryVersions.organizationId, input.organizationId),
        eq(crmDocumentLibraryVersions.isActive, true),
        isNull(crmDocumentLibraryVersions.deletedAt)
      )
    )
    .limit(1);

  if (!activeVersion) {
    throw new AuthError('No active document version found', 404);
  }

  return previewDocumentLibraryVersion({
    versionId: activeVersion.id,
    organizationId: input.organizationId,
    userId: input.userId
  });
}

export async function listDocumentLibraryPublishers(
  versionIds: string[]
): Promise<Record<string, string | null>> {
  if (!versionIds.length) {
    return {};
  }

  const rows = await db
    .select({
      id: users.id,
      name: users.name
    })
    .from(users)
    .where(
      inArray(
        users.id,
        (
          await db
            .select({ publishedBy: crmDocumentLibraryVersions.publishedBy })
            .from(crmDocumentLibraryVersions)
            .where(inArray(crmDocumentLibraryVersions.id, versionIds))
        )
          .map((row) => row.publishedBy)
          .filter((value): value is string => typeof value === 'string')
      )
    );

  return Object.fromEntries(rows.map((row) => [row.id, row.name]));
}
