import { Injectable, BadRequestException } from "@nestjs/common";
import { EntryStatus, Visibility } from "@prisma/client";
import archiver from "archiver";
import AdmZip from "adm-zip";
import matter from "gray-matter";
import { Readable } from "stream";
import { PrismaService } from "../../prisma/prisma.service";
import { KnowledgeService } from "../knowledge.service";
import { ExportFormat } from "../dto";
import type { ImportResult } from "../dto";
import type { CreateEntryDto } from "../dto/create-entry.dto";

interface ExportEntry {
  id: string;
  slug: string;
  title: string;
  content: string;
  summary: string | null;
  status: EntryStatus;
  visibility: Visibility;
  tags: string[];
  createdAt: Date;
  updatedAt: Date;
}

/**
 * Service for handling knowledge entry import/export operations
 */
@Injectable()
export class ImportExportService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly knowledgeService: KnowledgeService
  ) {}

  /**
   * Import entries from uploaded file(s)
   * Accepts single .md file or .zip containing multiple .md files
   */
  async importEntries(
    workspaceId: string,
    userId: string,
    file: Express.Multer.File
  ): Promise<{ results: ImportResult[]; totalFiles: number; imported: number; failed: number }> {
    const results: ImportResult[] = [];

    try {
      if (file.mimetype === "text/markdown" || file.originalname.endsWith(".md")) {
        // Single markdown file
        const result = await this.importSingleMarkdown(
          workspaceId,
          userId,
          file.originalname,
          file.buffer.toString("utf-8")
        );
        results.push(result);
      } else if (
        file.mimetype === "application/zip" ||
        file.mimetype === "application/x-zip-compressed" ||
        file.originalname.endsWith(".zip")
      ) {
        // Zip file containing multiple markdown files
        const zipResults = await this.importZipFile(workspaceId, userId, file.buffer);
        results.push(...zipResults);
      } else {
        throw new BadRequestException("Invalid file type. Only .md and .zip files are accepted.");
      }
    } catch (error) {
      throw new BadRequestException(
        `Failed to import file: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }

    const imported = results.filter((r) => r.success).length;
    const failed = results.filter((r) => !r.success).length;

    return {
      results,
      totalFiles: results.length,
      imported,
      failed,
    };
  }

  /**
   * Import a single markdown file
   */
  private async importSingleMarkdown(
    workspaceId: string,
    userId: string,
    filename: string,
    content: string
  ): Promise<ImportResult> {
    try {
      // Parse frontmatter
      const parsed = matter(content);
      const frontmatter = parsed.data;
      const markdownContent = parsed.content.trim();

      if (!markdownContent) {
        return {
          filename,
          success: false,
          error: "Empty content",
        };
      }

      // Build CreateEntryDto from frontmatter and content
      const parsedStatus = this.parseStatus(frontmatter.status as string | undefined);
      const parsedVisibility = this.parseVisibility(frontmatter.visibility as string | undefined);
      const parsedTags = Array.isArray(frontmatter.tags)
        ? (frontmatter.tags as string[])
        : undefined;

      const createDto: CreateEntryDto = {
        title:
          typeof frontmatter.title === "string" ? frontmatter.title : filename.replace(/\.md$/, ""),
        content: markdownContent,
        changeNote: "Imported from markdown file",
        ...(typeof frontmatter.summary === "string" && { summary: frontmatter.summary }),
        ...(parsedStatus && { status: parsedStatus }),
        ...(parsedVisibility && { visibility: parsedVisibility }),
        ...(parsedTags && { tags: parsedTags }),
      };

      // Create the entry
      const entry = await this.knowledgeService.create(workspaceId, userId, createDto);

      return {
        filename,
        success: true,
        entryId: entry.id,
        slug: entry.slug,
        title: entry.title,
      };
    } catch (error) {
      return {
        filename,
        success: false,
        error: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }

  /**
   * Import entries from a zip file
   */
  private async importZipFile(
    workspaceId: string,
    userId: string,
    buffer: Buffer
  ): Promise<ImportResult[]> {
    const results: ImportResult[] = [];
    const MAX_FILES = 1000; // Prevent zip bomb attacks
    const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100MB total uncompressed

    try {
      const zip = new AdmZip(buffer);
      const zipEntries = zip.getEntries();

      // Security: Check for zip bombs
      let totalUncompressedSize = 0;
      let fileCount = 0;

      for (const entry of zipEntries) {
        if (!entry.isDirectory) {
          fileCount++;
          totalUncompressedSize += entry.header.size;
        }
      }

      if (fileCount > MAX_FILES) {
        throw new BadRequestException(
          `Zip file contains too many files (${fileCount.toString()}). Maximum allowed: ${MAX_FILES.toString()}`
        );
      }

      if (totalUncompressedSize > MAX_TOTAL_SIZE) {
        throw new BadRequestException(
          `Zip file is too large when uncompressed (${Math.round(totalUncompressedSize / 1024 / 1024).toString()}MB). Maximum allowed: ${Math.round(MAX_TOTAL_SIZE / 1024 / 1024).toString()}MB`
        );
      }

      for (const zipEntry of zipEntries) {
        // Skip directories and non-markdown files
        if (zipEntry.isDirectory || !zipEntry.entryName.endsWith(".md")) {
          continue;
        }

        // Security: Prevent path traversal attacks
        const normalizedPath = zipEntry.entryName.replace(/\\/g, "/");
        if (
          normalizedPath.includes("..") ||
          normalizedPath.startsWith("/") ||
          normalizedPath.includes("//")
        ) {
          results.push({
            filename: zipEntry.entryName,
            success: false,
            error: "Invalid file path detected (potential path traversal)",
          });
          continue;
        }

        const content = zipEntry.getData().toString("utf-8");
        const result = await this.importSingleMarkdown(
          workspaceId,
          userId,
          zipEntry.entryName,
          content
        );
        results.push(result);
      }
    } catch (error) {
      throw new BadRequestException(
        `Failed to extract zip file: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    }

    return results;
  }

  /**
   * Export entries as a zip file
   */
  async exportEntries(
    workspaceId: string,
    format: ExportFormat,
    entryIds?: string[]
  ): Promise<{ stream: Readable; filename: string }> {
    // Fetch entries
    const entries = await this.fetchEntriesForExport(workspaceId, entryIds);

    if (entries.length === 0) {
      throw new BadRequestException("No entries found to export");
    }

    // Create archive
    const archive = archiver("zip", {
      zlib: { level: 9 },
    });

    // Add entries to archive
    for (const entry of entries) {
      if (format === ExportFormat.MARKDOWN) {
        const markdown = this.entryToMarkdown(entry);
        const filename = `${entry.slug}.md`;
        archive.append(markdown, { name: filename });
      } else {
        // JSON format
        const json = JSON.stringify(entry, null, 2);
        const filename = `${entry.slug}.json`;
        archive.append(json, { name: filename });
      }
    }

    // Finalize archive
    void archive.finalize();

    // Generate filename
    const timestamp = new Date().toISOString().split("T")[0] ?? "unknown";
    const filename = `knowledge-export-${timestamp}.zip`;

    return {
      stream: archive,
      filename,
    };
  }

  /**
   * Fetch entries for export
   */
  private async fetchEntriesForExport(
    workspaceId: string,
    entryIds?: string[]
  ): Promise<ExportEntry[]> {
    const where: Record<string, unknown> = { workspaceId };

    if (entryIds && entryIds.length > 0) {
      where.id = { in: entryIds };
    }

    const entries = await this.prisma.knowledgeEntry.findMany({
      where,
      include: {
        tags: {
          include: {
            tag: true,
          },
        },
      },
      orderBy: {
        title: "asc",
      },
    });

    return entries.map((entry) => ({
      id: entry.id,
      slug: entry.slug,
      title: entry.title,
      content: entry.content,
      summary: entry.summary,
      status: entry.status,
      visibility: entry.visibility,
      tags: entry.tags.map((et) => et.tag.name),
      createdAt: entry.createdAt,
      updatedAt: entry.updatedAt,
    }));
  }

  /**
   * Convert entry to markdown format with frontmatter
   */
  private entryToMarkdown(entry: ExportEntry): string {
    const frontmatter: Record<string, string | string[] | undefined> = {
      title: entry.title,
      status: entry.status,
      visibility: entry.visibility,
    };

    if (entry.summary) {
      frontmatter.summary = entry.summary;
    }

    if (entry.tags.length > 0) {
      frontmatter.tags = entry.tags;
    }

    frontmatter.createdAt = entry.createdAt.toISOString();
    frontmatter.updatedAt = entry.updatedAt.toISOString();

    // Build frontmatter string
    const frontmatterStr = Object.entries(frontmatter)
      .map(([key, value]) => {
        if (Array.isArray(value)) {
          return `${key}:\n  - ${value.join("\n  - ")}`;
        }
        return `${key}: ${String(value)}`;
      })
      .join("\n");

    return `---\n${frontmatterStr}\n---\n\n${entry.content}`;
  }

  /**
   * Parse status from frontmatter
   */
  private parseStatus(value: unknown): EntryStatus | undefined {
    if (!value || typeof value !== "string") return undefined;
    const statusMap: Record<string, EntryStatus> = {
      DRAFT: EntryStatus.DRAFT,
      PUBLISHED: EntryStatus.PUBLISHED,
      ARCHIVED: EntryStatus.ARCHIVED,
    };
    return statusMap[value.toUpperCase()];
  }

  /**
   * Parse visibility from frontmatter
   */
  private parseVisibility(value: unknown): Visibility | undefined {
    if (!value || typeof value !== "string") return undefined;
    const visibilityMap: Record<string, Visibility> = {
      PRIVATE: Visibility.PRIVATE,
      WORKSPACE: Visibility.WORKSPACE,
      PUBLIC: Visibility.PUBLIC,
    };
    return visibilityMap[value.toUpperCase()];
  }
}
