import { and, desc, eq } from 'drizzle-orm';

import { db } from '@/lib/db';
import { generatedReports } from '@/lib/db/schema/operations';
import { notifications } from '@/lib/db/schema/operations';
import { files } from '@/lib/db/schema/documents';
import { ports } from '@/lib/db/schema/ports';
import { generatePdf } from '@/lib/pdf/generate';
import { minioClient, getPresignedUrl, buildStoragePath } from '@/lib/minio/index';
import { emitToRoom } from '@/lib/socket/server';
import { getQueue } from '@/lib/queue';
import { env } from '@/lib/env';
import { logger } from '@/lib/logger';
import { NotFoundError } from '@/lib/errors';

import {
  fetchPipelineData,
  fetchRevenueData,
  fetchActivityData,
  fetchOccupancyData,
} from '@/lib/services/report-generators';
import {
  pipelineReportTemplate,
  buildPipelineInputs,
} from '@/lib/pdf/templates/reports/pipeline-report';
import {
  revenueReportTemplate,
  buildRevenueInputs,
} from '@/lib/pdf/templates/reports/revenue-report';
import {
  activityReportTemplate,
  buildActivityInputs,
} from '@/lib/pdf/templates/reports/activity-report';
import {
  occupancyReportTemplate,
  buildOccupancyInputs,
} from '@/lib/pdf/templates/reports/occupancy-report';

import type { RequestReportInput, ListReportsInput } from '@/lib/validators/reports';

// ─── Report Type Map ──────────────────────────────────────────────────────────

const REPORT_TYPE_MAP = {
  pipeline: {
    fetchData: fetchPipelineData,
    template: pipelineReportTemplate,
    buildInputs: buildPipelineInputs,
  },
  revenue: {
    fetchData: fetchRevenueData,
    template: revenueReportTemplate,
    buildInputs: buildRevenueInputs,
  },
  activity: {
    fetchData: fetchActivityData,
    template: activityReportTemplate,
    buildInputs: buildActivityInputs,
  },
  occupancy: {
    fetchData: fetchOccupancyData,
    template: occupancyReportTemplate,
    buildInputs: buildOccupancyInputs,
  },
} as const;

type ReportType = keyof typeof REPORT_TYPE_MAP;

// ─── requestReport ────────────────────────────────────────────────────────────

export async function requestReport(
  portId: string,
  userId: string,
  data: RequestReportInput,
) {
  const [report] = await db
    .insert(generatedReports)
    .values({
      portId,
      reportType: data.reportType,
      name: data.name,
      status: 'queued',
      parameters: data.parameters ?? {},
      requestedBy: userId,
    })
    .returning();

  if (!report) {
    throw new Error('Failed to create report record');
  }

  await getQueue('reports').add('generate-report', { reportJobId: report.id });

  emitToRoom(`user:${userId}`, 'report:queued', {
    reportId: report.id,
    reportType: report.reportType,
    name: report.name,
  });

  return report;
}

// ─── listReports ──────────────────────────────────────────────────────────────

export async function listReports(portId: string, query: ListReportsInput) {
  const conditions = [eq(generatedReports.portId, portId)];
  if (query.status) {
    conditions.push(eq(generatedReports.status, query.status));
  }

  const offset = (query.page - 1) * query.limit;

  const [rows, countResult] = await Promise.all([
    db
      .select()
      .from(generatedReports)
      .where(and(...conditions))
      .orderBy(desc(generatedReports.createdAt))
      .limit(query.limit)
      .offset(offset),
    db.$count(generatedReports, and(...conditions)),
  ]);

  return {
    data: rows,
    total: Number(countResult),
  };
}

// ─── getReport ────────────────────────────────────────────────────────────────

export async function getReport(reportId: string, portId: string) {
  const report = await db.query.generatedReports.findFirst({
    where: and(eq(generatedReports.id, reportId), eq(generatedReports.portId, portId)),
  });

  if (!report) {
    throw new NotFoundError('Report');
  }

  return report;
}

// ─── getDownloadUrl ───────────────────────────────────────────────────────────

export async function getDownloadUrl(reportId: string, portId: string) {
  const report = await db.query.generatedReports.findFirst({
    where: and(eq(generatedReports.id, reportId), eq(generatedReports.portId, portId)),
  });

  if (!report) {
    throw new NotFoundError('Report');
  }

  if (report.status !== 'ready' || !report.fileId) {
    throw new Error('Report is not ready for download');
  }

  const file = await db.query.files.findFirst({
    where: eq(files.id, report.fileId),
  });

  if (!file) {
    throw new NotFoundError('File');
  }

  const url = await getPresignedUrl(file.storagePath);
  return { url };
}

// ─── generateReport ───────────────────────────────────────────────────────────

export async function generateReport(reportJobId: string): Promise<void> {
  // 1. Fetch the generatedReports record
  const report = await db.query.generatedReports.findFirst({
    where: eq(generatedReports.id, reportJobId),
  });

  if (!report) {
    throw new Error(`Report job not found: ${reportJobId}`);
  }

  const { portId, reportType, name, parameters, requestedBy } = report;

  try {
    // 2. Update status = 'processing', startedAt = now
    await db
      .update(generatedReports)
      .set({ status: 'processing', startedAt: new Date() })
      .where(eq(generatedReports.id, reportJobId));

    // 3. Look up REPORT_TYPE_MAP[reportType]
    const typeKey = reportType as ReportType;
    const config = REPORT_TYPE_MAP[typeKey];
    if (!config) {
      throw new Error(`Unknown report type: ${reportType}`);
    }

    const params = (parameters ?? {}) as Record<string, unknown>;

    // 4. Fetch data
    const data = await config.fetchData(portId, params);

    // 5. Get port info for name in PDF
    const port = await db.query.ports.findFirst({
      where: eq(ports.id, portId),
    });
    const portName = port?.name ?? 'Port Nimara';
    const portSlug = port?.slug ?? 'port';

    // 6. Build inputs (pass portName)
    const inputs = (config.buildInputs as (data: unknown, portName: string) => Record<string, string>[])(data, portName);

    // 7. Generate PDF
    const pdfBytes = await generatePdf(config.template, inputs);

    // 8. Build storage path
    const fileId = crypto.randomUUID();
    const storagePath = buildStoragePath(portSlug, 'reports', reportJobId, fileId, 'pdf');

    // 9. Upload PDF to MinIO
    const buffer = Buffer.from(pdfBytes);
    await minioClient.putObject(
      env.MINIO_BUCKET,
      storagePath,
      buffer,
      buffer.length,
      { 'Content-Type': 'application/pdf', 'report-type': reportType },
    );

    // 10. Insert into files table
    const [fileRecord] = await db
      .insert(files)
      .values({
        id: fileId,
        portId,
        filename: `${name.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${Date.now()}.pdf`,
        originalName: `${name}.pdf`,
        mimeType: 'application/pdf',
        sizeBytes: String(buffer.length),
        storagePath,
        storageBucket: env.MINIO_BUCKET,
        category: 'misc',
        uploadedBy: requestedBy,
      })
      .returning();

    if (!fileRecord) {
      throw new Error('Failed to insert file record');
    }

    // 11. Update generatedReports: status='ready', fileId, completedAt
    await db
      .update(generatedReports)
      .set({
        status: 'ready',
        fileId: fileRecord.id,
        completedAt: new Date(),
        updatedAt: new Date(),
      })
      .where(eq(generatedReports.id, reportJobId));

    // 12. Emit report:ready socket event
    emitToRoom(`user:${requestedBy}`, 'report:ready', {
      reportId: reportJobId,
      name,
    });

    // 13. Create notification for requestedBy user
    await db.insert(notifications).values({
      portId,
      userId: requestedBy,
      type: 'system_alert',
      title: 'Report Ready',
      description: `Your report "${name}" is ready to download.`,
      entityType: 'report',
      entityId: reportJobId,
    });

    logger.info({ reportJobId, reportType }, 'Report generated successfully');
  } catch (err) {
    const errorMessage = err instanceof Error ? err.message : 'Unknown error';
    logger.error({ reportJobId, err }, 'Report generation failed');

    await db
      .update(generatedReports)
      .set({
        status: 'failed',
        errorMessage,
        updatedAt: new Date(),
      })
      .where(eq(generatedReports.id, reportJobId));

    emitToRoom(`user:${requestedBy}`, 'report:failed', {
      reportId: reportJobId,
      name,
      error: errorMessage,
    });

    throw err;
  }
}
