import { FastifyInstance } from "fastify";
import { z } from "zod";
import { randomBytes } from "crypto";
import { db } from "../db/client.js";
import { medications, userSettings, doseTracking, shareTokens } from "../db/schema.js";
import { eq } from "drizzle-orm";
import { requireAuth, getAnonymousUserId } from "../plugins/auth.js";
import { env } from "../plugins/env.js";
import type { AuthUser } from "../types/fastify.js";
import { resolve, extname } from "path";
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";

const IMAGES_DIR = resolve(process.cwd(), "data/images");

// =============================================================================
// Export Format Version (bump this when format changes)
// =============================================================================
const EXPORT_VERSION = "1.0";

// =============================================================================
// Zod Schemas for Import Validation
// =============================================================================

const scheduleSchema = z.object({
  usage: z.number().nonnegative(),
  every: z.number().int().min(1),
  start: z.string(), // ISO datetime string
  remind: z.boolean().optional().default(false),
});

const inventorySchema = z.object({
  packCount: z.number().int().min(0).default(1),
  blistersPerPack: z.number().int().min(1).default(1),
  pillsPerBlister: z.number().int().min(1).default(1),
  looseTablets: z.number().int().min(0).default(0),
});

const medicationExportSchema = z.object({
  _exportId: z.string(),
  name: z.string().min(1),
  genericName: z.string().nullable().optional(),
  takenBy: z.array(z.string()).default([]),
  inventory: inventorySchema,
  pillWeightMg: z.number().int().nullable().optional(),
  schedules: z.array(scheduleSchema).default([]),
  expiryDate: z.string().nullable().optional(),
  notes: z.string().nullable().optional(),
  intakeRemindersEnabled: z.boolean().default(false),
  image: z.string().nullable().optional(), // base64 data URL or null
});

const doseHistorySchema = z.object({
  medicationRef: z.string(), // References _exportId
  scheduleIndex: z.number().int().min(0),
  scheduledTime: z.string(), // ISO datetime
  takenAt: z.string(), // ISO datetime
  markedBy: z.string().nullable().optional(),
});

const shareLinkSchema = z.object({
  takenBy: z.string().min(1),
  scheduleDays: z.number().int().min(1).default(30),
  expiresAt: z.string().nullable().optional(), // ISO datetime
  regenerateToken: z.boolean().default(true),
});

const settingsExportSchema = z.object({
  // Email notifications
  emailEnabled: z.boolean().default(false),
  notificationEmail: z.string().nullable().optional(),
  emailStockReminders: z.boolean().default(true),
  emailIntakeReminders: z.boolean().default(true),
  // Push notifications
  shoutrrrEnabled: z.boolean().optional(),
  shoutrrrUrl: z.string().nullable().optional(),
  shoutrrrStockReminders: z.boolean().default(true),
  shoutrrrIntakeReminders: z.boolean().default(true),
  // Reminder settings
  reminderDaysBefore: z.number().int().default(7),
  repeatDailyReminders: z.boolean().default(false),
  skipRemindersForTakenDoses: z.boolean().default(false),
  repeatRemindersEnabled: z.boolean().default(false),
  reminderRepeatIntervalMinutes: z.number().int().default(30),
  maxNaggingReminders: z.number().int().default(5),
  // Stock thresholds
  lowStockDays: z.number().int().default(30),
  normalStockDays: z.number().int().default(90),
  highStockDays: z.number().int().default(180),
  // UI preferences
  language: z.string().default("en"),
  stockCalculationMode: z.enum(["automatic", "manual"]).default("automatic"),
}).optional();

const importDataSchema = z.object({
  version: z.string(),
  exportedAt: z.string(),
  includeSensitiveData: z.boolean().default(false),
  medications: z.array(medicationExportSchema).default([]),
  doseHistory: z.array(doseHistorySchema).default([]),
  settings: settingsExportSchema,
  shareLinks: z.array(shareLinkSchema).default([]),
});

// =============================================================================
// Helper Functions
// =============================================================================

// Helper to get user ID from request
async function getUserId(request: any, reply: any): Promise<number> {
  if (!env.AUTH_ENABLED) {
    return getAnonymousUserId();
  }
  
  const authUser = request.user as unknown as AuthUser | null;
  if (!authUser) {
    reply.status(401).send({ error: "Not authenticated" });
    throw new Error("AUTH_REQUIRED");
  }
  return authUser.id;
}

// Parse takenByJson safely
function parseTakenByJson(takenByJson: string | null | undefined): string[] {
  if (!takenByJson) return [];
  try {
    const parsed = JSON.parse(takenByJson);
    return Array.isArray(parsed) ? parsed.filter((s: unknown) => typeof s === "string" && s.trim()) : [];
  } catch {
    return [];
  }
}

// Parse blisters from DB format to export format
function parseBlistersForExport(row: typeof medications.$inferSelect): Array<{ usage: number; every: number; start: string; remind: boolean }> {
  try {
    const usage = JSON.parse(row.usageJson || "[]") as number[];
    const every = JSON.parse(row.everyJson || "[]") as number[];
    const start = JSON.parse(row.startJson || "[]") as string[];
    const len = Math.min(usage.length, every.length, start.length);
    const schedules: Array<{ usage: number; every: number; start: string; remind: boolean }> = [];
    for (let i = 0; i < len; i++) {
      schedules.push({
        usage: usage[i],
        every: every[i],
        start: start[i],
        remind: row.intakeRemindersEnabled ?? false,
      });
    }
    return schedules;
  } catch {
    return [];
  }
}

// Read image file and convert to base64 data URL
function imageToBase64(imageUrl: string | null): string | null {
  if (!imageUrl) return null;
  const imagePath = resolve(IMAGES_DIR, imageUrl);
  if (!existsSync(imagePath)) return null;
  
  try {
    const imageBuffer = readFileSync(imagePath);
    const ext = extname(imageUrl).toLowerCase();
    const mimeTypes: Record<string, string> = {
      ".jpg": "image/jpeg",
      ".jpeg": "image/jpeg",
      ".png": "image/png",
      ".webp": "image/webp",
      ".gif": "image/gif",
    };
    const mimeType = mimeTypes[ext] || "image/jpeg";
    return `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
  } catch {
    return null;
  }
}

// Save base64 image to file and return filename
function base64ToImage(base64: string, medicationId: number): string | null {
  if (!base64 || !base64.startsWith("data:")) return null;
  
  try {
    // Parse data URL: "data:image/jpeg;base64,/9j/4AAQ..."
    const matches = base64.match(/^data:image\/(\w+);base64,(.+)$/);
    if (!matches) return null;
    
    const ext = matches[1] === "jpeg" ? "jpg" : matches[1];
    const data = matches[2];
    const buffer = Buffer.from(data, "base64");
    
    const filename = `med-${medicationId}-${Date.now()}.${ext}`;
    const filepath = resolve(IMAGES_DIR, filename);
    
    // Ensure images directory exists
    if (!existsSync(IMAGES_DIR)) {
      mkdirSync(IMAGES_DIR, { recursive: true });
    }
    
    writeFileSync(filepath, buffer);
    return filename;
  } catch {
    return null;
  }
}

// Parse dose ID to extract medication ID and timestamp
// Format: "{medicationId}-{blisterIndex}-{timestampMs}"
function parseDoseId(doseId: string): { medicationId: number; blisterIndex: number; timestampMs: number } | null {
  const parts = doseId.split("-");
  if (parts.length < 3) return null;
  
  const medicationId = parseInt(parts[0], 10);
  const blisterIndex = parseInt(parts[1], 10);
  const timestampMs = parseInt(parts[2], 10);
  
  if (isNaN(medicationId) || isNaN(blisterIndex) || isNaN(timestampMs)) return null;
  
  return { medicationId, blisterIndex, timestampMs };
}

// Build dose ID from parts
function buildDoseId(medicationId: number, blisterIndex: number, timestampMs: number): string {
  return `${medicationId}-${blisterIndex}-${timestampMs}`;
}

// =============================================================================
// Export Routes
// =============================================================================
export async function exportRoutes(app: FastifyInstance) {
  // All export routes require auth
  app.addHook("preHandler", requireAuth);

  // ---------------------------------------------------------------------------
  // GET /export - Export all user data
  // ---------------------------------------------------------------------------
  app.get<{ Querystring: { includeSensitive?: string } }>(
    "/export",
    async (request, reply) => {
      const userId = await getUserId(request, reply);
      const includeSensitive = request.query.includeSensitive === "true";

      // 1. Load all medications
      const meds = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id);
      
      // Build medication ID to export ID mapping
      const medIdToExportId = new Map<number, string>();
      const exportMedications = meds.map((med, index) => {
        const exportId = `med-${index + 1}`;
        medIdToExportId.set(med.id, exportId);
        
        return {
          _exportId: exportId,
          name: med.name,
          genericName: med.genericName,
          takenBy: parseTakenByJson(med.takenByJson),
          inventory: {
            packCount: med.packCount ?? 1,
            blistersPerPack: med.blistersPerPack ?? 1,
            pillsPerBlister: med.pillsPerBlister ?? 1,
            looseTablets: med.looseTablets ?? 0,
          },
          pillWeightMg: med.pillWeightMg,
          schedules: parseBlistersForExport(med),
          expiryDate: med.expiryDate,
          notes: med.notes,
          intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
          image: imageToBase64(med.imageUrl),
        };
      });

      // 2. Load all dose tracking entries
      const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, userId));
      
      const exportDoseHistory = doses.map((dose) => {
        const parsed = parseDoseId(dose.doseId);
        if (!parsed) return null;
        
        const exportId = medIdToExportId.get(parsed.medicationId);
        if (!exportId) return null; // Orphaned dose, skip
        
        return {
          medicationRef: exportId,
          scheduleIndex: parsed.blisterIndex,
          scheduledTime: new Date(parsed.timestampMs).toISOString(),
          takenAt: dose.takenAt?.toISOString() ?? new Date().toISOString(),
          markedBy: dose.markedBy,
        };
      }).filter((d): d is NonNullable<typeof d> => d !== null);

      // 3. Load user settings
      const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, userId));
      
      const exportSettings = settings ? {
        emailEnabled: settings.emailEnabled,
        notificationEmail: settings.notificationEmail,
        emailStockReminders: settings.emailStockReminders,
        emailIntakeReminders: settings.emailIntakeReminders,
        // Only include sensitive data if requested
        shoutrrrEnabled: includeSensitive ? settings.shoutrrrEnabled : undefined,
        shoutrrrUrl: includeSensitive ? settings.shoutrrrUrl : undefined,
        shoutrrrStockReminders: settings.shoutrrrStockReminders,
        shoutrrrIntakeReminders: settings.shoutrrrIntakeReminders,
        reminderDaysBefore: settings.reminderDaysBefore,
        repeatDailyReminders: settings.repeatDailyReminders,
        skipRemindersForTakenDoses: settings.skipRemindersForTakenDoses,
        repeatRemindersEnabled: settings.repeatRemindersEnabled,
        reminderRepeatIntervalMinutes: settings.reminderRepeatIntervalMinutes,
        maxNaggingReminders: settings.maxNaggingReminders,
        lowStockDays: settings.lowStockDays,
        normalStockDays: settings.normalStockDays,
        highStockDays: settings.highStockDays,
        language: settings.language,
        stockCalculationMode: settings.stockCalculationMode,
      } : undefined;

      // 4. Load share links
      const shares = await db.select().from(shareTokens).where(eq(shareTokens.userId, userId));
      
      const exportShareLinks = shares.map((share) => ({
        takenBy: share.takenBy,
        scheduleDays: share.scheduleDays,
        expiresAt: share.expiresAt?.toISOString() ?? null,
        regenerateToken: true, // Always regenerate tokens on import for security
      }));

      // Build export object
      const exportData = {
        version: EXPORT_VERSION,
        exportedAt: new Date().toISOString(),
        includeSensitiveData: includeSensitive,
        medications: exportMedications,
        doseHistory: exportDoseHistory,
        settings: exportSettings,
        shareLinks: exportShareLinks,
      };

      // Set download headers
      const filename = `medassist-export-${new Date().toISOString().split("T")[0]}.json`;
      reply.header("Content-Type", "application/json");
      reply.header("Content-Disposition", `attachment; filename="${filename}"`);
      
      return exportData;
    }
  );

  // ---------------------------------------------------------------------------
  // POST /import - Import user data (replaces all existing data!)
  // ---------------------------------------------------------------------------
  app.post(
    "/import",
    async (request, reply) => {
      const userId = await getUserId(request, reply);

      // 1. Parse and validate import data
      const parsed = importDataSchema.safeParse(request.body);
      if (!parsed.success) {
        return reply.status(400).send({
          error: "Invalid import data format",
          details: parsed.error.format(),
        });
      }

      const importData = parsed.data;

      // 2. Delete all existing user data (in correct order to respect foreign keys)
      // Note: CASCADE delete should handle this, but let's be explicit
      
      // First, delete images for existing medications
      const existingMeds = await db.select().from(medications).where(eq(medications.userId, userId));
      for (const med of existingMeds) {
        if (med.imageUrl) {
          const imagePath = resolve(IMAGES_DIR, med.imageUrl);
          if (existsSync(imagePath)) {
            try { unlinkSync(imagePath); } catch { /* ignore */ }
          }
        }
      }

      // Delete in order: doses, share tokens, medications, settings
      await db.delete(doseTracking).where(eq(doseTracking.userId, userId));
      await db.delete(shareTokens).where(eq(shareTokens.userId, userId));
      await db.delete(medications).where(eq(medications.userId, userId));
      await db.delete(userSettings).where(eq(userSettings.userId, userId));

      // 3. Import medications and build ID mapping
      const exportIdToNewId = new Map<string, number>();
      
      for (const med of importData.medications) {
        // Convert schedules back to JSON arrays
        const usageJson = JSON.stringify(med.schedules.map((s) => s.usage));
        const everyJson = JSON.stringify(med.schedules.map((s) => s.every));
        const startJson = JSON.stringify(med.schedules.map((s) => s.start));
        const takenByJson = JSON.stringify(med.takenBy);
        
        // Check if any schedule has remind enabled
        const intakeRemindersEnabled = med.schedules.some((s) => s.remind) || med.intakeRemindersEnabled;

        const [inserted] = await db.insert(medications).values({
          userId,
          name: med.name,
          genericName: med.genericName || null,
          takenByJson,
          packCount: med.inventory.packCount,
          blistersPerPack: med.inventory.blistersPerPack,
          pillsPerBlister: med.inventory.pillsPerBlister,
          looseTablets: med.inventory.looseTablets,
          pillWeightMg: med.pillWeightMg || null,
          usageJson,
          everyJson,
          startJson,
          expiryDate: med.expiryDate || null,
          notes: med.notes || null,
          intakeRemindersEnabled,
          imageUrl: null, // Will be set after image is saved
        }).returning();

        // Save mapping
        exportIdToNewId.set(med._exportId, inserted.id);

        // Save image if present
        if (med.image) {
          const imageUrl = base64ToImage(med.image, inserted.id);
          if (imageUrl) {
            await db.update(medications)
              .set({ imageUrl })
              .where(eq(medications.id, inserted.id));
          }
        }
      }

      // 4. Import dose history with remapped medication IDs
      for (const dose of importData.doseHistory) {
        const newMedId = exportIdToNewId.get(dose.medicationRef);
        if (!newMedId) continue; // Skip orphaned doses
        
        // Convert ISO timestamp back to milliseconds for dose ID
        const timestampMs = new Date(dose.scheduledTime).getTime();
        const doseId = buildDoseId(newMedId, dose.scheduleIndex, timestampMs);
        
        await db.insert(doseTracking).values({
          userId,
          doseId,
          takenAt: new Date(dose.takenAt),
          markedBy: dose.markedBy || null,
        });
      }

      // 5. Import settings
      if (importData.settings) {
        await db.insert(userSettings).values({
          userId,
          emailEnabled: importData.settings.emailEnabled ?? false,
          notificationEmail: importData.settings.notificationEmail || null,
          emailStockReminders: importData.settings.emailStockReminders ?? true,
          emailIntakeReminders: importData.settings.emailIntakeReminders ?? true,
          shoutrrrEnabled: importData.settings.shoutrrrEnabled ?? false,
          shoutrrrUrl: importData.settings.shoutrrrUrl || null,
          shoutrrrStockReminders: importData.settings.shoutrrrStockReminders ?? true,
          shoutrrrIntakeReminders: importData.settings.shoutrrrIntakeReminders ?? true,
          reminderDaysBefore: importData.settings.reminderDaysBefore ?? 7,
          repeatDailyReminders: importData.settings.repeatDailyReminders ?? false,
          skipRemindersForTakenDoses: importData.settings.skipRemindersForTakenDoses ?? false,
          repeatRemindersEnabled: importData.settings.repeatRemindersEnabled ?? false,
          reminderRepeatIntervalMinutes: importData.settings.reminderRepeatIntervalMinutes ?? 30,
          maxNaggingReminders: importData.settings.maxNaggingReminders ?? 5,
          lowStockDays: importData.settings.lowStockDays ?? 30,
          normalStockDays: importData.settings.normalStockDays ?? 90,
          highStockDays: importData.settings.highStockDays ?? 180,
          language: importData.settings.language ?? "en",
          stockCalculationMode: importData.settings.stockCalculationMode ?? "automatic",
        });
      }

      // 6. Import share links (with new tokens)
      for (const share of importData.shareLinks) {
        // Always generate new token for security
        const token = randomBytes(8).toString("hex");
        
        await db.insert(shareTokens).values({
          userId,
          token,
          takenBy: share.takenBy,
          scheduleDays: share.scheduleDays,
          expiresAt: share.expiresAt ? new Date(share.expiresAt) : null,
        });
      }

      return {
        success: true,
        imported: {
          medications: importData.medications.length,
          doseHistory: importData.doseHistory.length,
          settings: importData.settings ? 1 : 0,
          shareLinks: importData.shareLinks.length,
        },
      };
    }
  );
}
