import fs from "node:fs/promises";
import path from "node:path";
import crypto from "node:crypto";
import { query } from "../../db/pool.js";
import { env } from "../../config/env.js";
import { receiptsStorageDir } from "../../lib/storage.js";
import { recordAppEvent } from "../../lib/app-log.js";
import { sendOutboxEmail } from "../../lib/mailer.js";
import { findUserById, isActiveAccount, type UserRow, safeUser } from "../auth/auth.service.js";
import { getUserLegalAcceptances } from "../legal/legal.service.js";

function hashToken(token: string): string {
  return crypto.createHash("sha256").update(token).digest("hex");
}

function buildCancelUrl(token: string): string {
  return `${env.WEB_BASE_URL}/login?cancel=${encodeURIComponent(token)}`;
}

function buildDeleteConfirmationBody(name: string, cancelUrl: string): string {
  return [
    `Hola ${name},`,
    "",
    "Hemos recibido la solicitud de eliminación de tu cuenta.",
    "Tu cuenta quedará pendiente de borrado durante 30 días.",
    `Si quieres cancelar la baja, entra aquí: ${cancelUrl}`,
    "",
    "Si no haces nada, la información se eliminará automáticamente al terminar el plazo.",
  ].join("\n");
}

async function logLifecycleEvent(level: "info" | "warning" | "error", message: string, details: Record<string, unknown>): Promise<void> {
  await recordAppEvent({
    level,
    source: "account.lifecycle",
    message,
    details,
  });
}

export interface NotificationPreferences {
  warrantyEmailEnabled: boolean;
  warrantyPushEnabled: boolean;
}

export interface PushSubscriptionInput {
  endpoint: string;
  p256dhKey: string;
  authKey: string;
  userAgent?: string;
}

export interface PushSubscriptionRow {
  endpoint: string;
  p256dh_key: string;
  auth_key: string;
  user_agent: string | null;
}

export async function getNotificationPreferences(userId: number): Promise<NotificationPreferences> {
  const rows = await query<{
    warranty_email_enabled: boolean;
    warranty_push_enabled: boolean;
  }>(
    `INSERT INTO notification_preferences (user_id)
     VALUES ($1)
     ON CONFLICT (user_id) DO UPDATE SET user_id = EXCLUDED.user_id
     RETURNING warranty_email_enabled, warranty_push_enabled`,
    [userId]
  );
  const prefs = rows[0];
  return {
    warrantyEmailEnabled: Boolean(prefs?.warranty_email_enabled),
    warrantyPushEnabled: Boolean(prefs?.warranty_push_enabled),
  };
}

export async function updateNotificationPreferences(
  userId: number,
  input: NotificationPreferences
): Promise<NotificationPreferences> {
  const rows = await query<{
    warranty_email_enabled: boolean;
    warranty_push_enabled: boolean;
  }>(
    `INSERT INTO notification_preferences (
       user_id, warranty_email_enabled, warranty_push_enabled
     ) VALUES ($1, $2, $3)
     ON CONFLICT (user_id) DO UPDATE SET
       warranty_email_enabled = EXCLUDED.warranty_email_enabled,
       warranty_push_enabled = EXCLUDED.warranty_push_enabled,
       updated_at = NOW()
     RETURNING warranty_email_enabled, warranty_push_enabled`,
    [userId, input.warrantyEmailEnabled, input.warrantyPushEnabled]
  );
  const prefs = rows[0];
  return {
    warrantyEmailEnabled: Boolean(prefs?.warranty_email_enabled),
    warrantyPushEnabled: Boolean(prefs?.warranty_push_enabled),
  };
}

export async function savePushSubscription(userId: number, input: PushSubscriptionInput): Promise<void> {
  await query(
    `INSERT INTO push_subscriptions (user_id, endpoint, p256dh_key, auth_key, user_agent, updated_at)
     VALUES ($1, $2, $3, $4, $5, NOW())
     ON CONFLICT (endpoint) DO UPDATE SET
       user_id = EXCLUDED.user_id,
       p256dh_key = EXCLUDED.p256dh_key,
       auth_key = EXCLUDED.auth_key,
       user_agent = EXCLUDED.user_agent,
       updated_at = NOW()`,
    [userId, input.endpoint, input.p256dhKey, input.authKey, input.userAgent ?? null]
  );
}

export async function deletePushSubscription(userId: number, endpoint: string): Promise<void> {
  await query(
    "DELETE FROM push_subscriptions WHERE user_id = $1 AND endpoint = $2",
    [userId, endpoint]
  );
}

export async function listPushSubscriptions(userId: number): Promise<PushSubscriptionRow[]> {
  return query<PushSubscriptionRow>(
    `SELECT endpoint, p256dh_key, auth_key, user_agent
     FROM push_subscriptions
     WHERE user_id = $1
     ORDER BY updated_at DESC`,
    [userId]
  );
}

export async function requestAccountDeletion(userId: number): Promise<{ scheduledFor: string; cancelUrl: string }> {
  const user = await findUserById(userId);
  if (!user) {
    throw new Error("USER_NOT_FOUND");
  }
  if (!isActiveAccount(user)) {
    throw new Error("ACCOUNT_NOT_ACTIVE");
  }

  const token = crypto.randomBytes(32).toString("base64url");
  const tokenHash = hashToken(token);
  const scheduledFor = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
  const cancelUrl = buildCancelUrl(token);

  await query(
    `INSERT INTO account_deletion_tokens (user_id, token_hash, expires_at)
     VALUES ($1, $2, $3)`,
    [user.id, tokenHash, scheduledFor]
  );

  const updatedRows = await query<UserRow>(
    `UPDATE users
     SET status = 'pending_deletion',
         deletion_requested_at = NOW(),
         deletion_scheduled_for = $2
     WHERE id = $1 AND deleted_at IS NULL
     RETURNING *`,
    [user.id, scheduledFor]
  );
  const updated = updatedRows[0];
  if (!updated) {
    throw new Error("USER_NOT_FOUND");
  }

  const outbox = await query<{ id: number }>(
    `INSERT INTO email_outbox (user_id, recipient, subject, body)
     VALUES ($1, $2, $3, $4)
     RETURNING id`,
    [
      user.id,
      user.email,
      "Tu cuenta de TikeTrack quedará eliminada en 30 días",
      buildDeleteConfirmationBody(user.name, cancelUrl),
    ]
  );

  const outboxId = outbox[0]?.id;
  if (outboxId) {
    void sendOutboxEmail({
      id: outboxId,
      recipient: user.email,
      subject: "Tu cuenta de TikeTrack quedará eliminada en 30 días",
      body: buildDeleteConfirmationBody(user.name, cancelUrl),
    });
  }

  await logLifecycleEvent("warning", "Account deletion requested", {
    userId: user.id,
    email: user.email,
    scheduledFor,
  });

  return { scheduledFor, cancelUrl };
}

export async function cancelAccountDeletionByToken(token: string): Promise<UserRow | null> {
  const tokenHash = hashToken(token);
  const rows = await query<{ id: number; user_id: number }>(
    `SELECT id, user_id
     FROM account_deletion_tokens
     WHERE token_hash = $1
       AND consumed_at IS NULL
       AND expires_at > NOW()
     LIMIT 1`,
    [tokenHash]
  );

  const tokenRow = rows[0];
  if (!tokenRow) return null;

  await query("UPDATE account_deletion_tokens SET consumed_at = NOW() WHERE id = $1", [tokenRow.id]);

  const updatedRows = await query<UserRow>(
    `UPDATE users
     SET status = 'active',
         deletion_requested_at = NULL,
         deletion_scheduled_for = NULL
     WHERE id = $1 AND deleted_at IS NULL
     RETURNING *`,
    [tokenRow.user_id]
  );
  const updated = updatedRows[0];
  if (!updated) return null;

  const body = [
    `Hola ${updated.name},`,
    "",
    "La eliminación de tu cuenta ha sido cancelada correctamente.",
    "Tu cuenta vuelve a estar activa y puedes seguir usándola con normalidad.",
  ].join("\n");

  const outbox = await query<{ id: number }>(
    `INSERT INTO email_outbox (user_id, recipient, subject, body)
     VALUES ($1, $2, $3, $4)
     RETURNING id`,
    [
      updated.id,
      updated.email,
      "La eliminación de tu cuenta ha sido cancelada",
      body,
    ]
  );

  const outboxId = outbox[0]?.id;
  if (outboxId) {
    void sendOutboxEmail({
      id: outboxId,
      recipient: updated.email,
      subject: "La eliminación de tu cuenta ha sido cancelada",
      body,
    });
  }

  await logLifecycleEvent("info", "Account deletion cancelled", {
    userId: updated.id,
    email: updated.email,
  });

  return updated;
}

export async function listAccountsReadyForPurge(limit = 100): Promise<Array<{ id: number; email: string; name: string }>> {
  return query<{ id: number; email: string; name: string }>(
    `SELECT id, email, name
     FROM users
     WHERE status = 'pending_deletion'
       AND deleted_at IS NULL
       AND deletion_scheduled_for IS NOT NULL
       AND deletion_scheduled_for <= NOW()
     ORDER BY deletion_scheduled_for ASC
     LIMIT $1`,
    [limit]
  );
}

export async function purgeAccount(userId: number): Promise<boolean> {
  const receiptRows = await query<{ receipt_image: string | null }>(
    `SELECT receipt_image
     FROM products
     WHERE user_id = $1
       AND receipt_image IS NOT NULL`,
    [userId]
  );
  const mediaRows = await query<{ file_url: string }>(
    `SELECT file_url
     FROM product_media
     WHERE user_id = $1`,
    [userId]
  );
  const attachmentRows = await query<{ file_url: string }>(
    `SELECT file_url
     FROM incident_attachments
     WHERE user_id = $1`,
    [userId]
  );

  const deletedRows = await query<{ id: number; email: string; name: string }>(
    `DELETE FROM users
     WHERE id = $1
       AND status = 'pending_deletion'
       AND deletion_scheduled_for IS NOT NULL
       AND deletion_scheduled_for <= NOW()
     RETURNING id, email, name`,
    [userId]
  );

  const deleted = deletedRows[0];
  if (!deleted) return false;

  const filenames = new Set(
    receiptRows
      .map((row) => row.receipt_image)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );
  const mediaFilenames = new Set(
    mediaRows
      .map((row) => row.file_url)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );
  const attachmentFilenames = new Set(
    attachmentRows
      .map((row) => row.file_url)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );

  for (const filename of new Set([...filenames, ...mediaFilenames, ...attachmentFilenames])) {
    try {
      const candidates = [
        path.join(receiptsStorageDir, filename),
        path.join(path.dirname(receiptsStorageDir), "product-media", filename),
        path.join(path.dirname(receiptsStorageDir), "incident-attachments", filename),
      ];
      for (const candidate of candidates) {
        await fs.unlink(candidate).catch(() => {});
      }
    } catch (err) {
      const code = err instanceof Error && "code" in err ? (err as NodeJS.ErrnoException).code : null;
      if (code !== "ENOENT") {
        console.warn("[account/purge] receipt cleanup failed:", filename, err);
      }
    }
  }

  await logLifecycleEvent("warning", "Account purged", {
    userId: deleted.id,
    email: deleted.email,
    name: deleted.name,
    receiptFilesRemoved: filenames.size,
  });

  return true;
}

export async function purgeUserImmediately(userId: number): Promise<boolean> {
  const receiptRows = await query<{ receipt_image: string | null }>(
    `SELECT receipt_image
     FROM products
     WHERE user_id = $1
       AND receipt_image IS NOT NULL`,
    [userId]
  );
  const mediaRows = await query<{ file_url: string }>(
    `SELECT file_url
     FROM product_media
     WHERE user_id = $1`,
    [userId]
  );
  const attachmentRows = await query<{ file_url: string }>(
    `SELECT file_url
     FROM incident_attachments
     WHERE user_id = $1`,
    [userId]
  );

  const deletedRows = await query<{ id: number; email: string; name: string }>(
    `DELETE FROM users
     WHERE id = $1
       AND deleted_at IS NULL
     RETURNING id, email, name`,
    [userId]
  );

  const deleted = deletedRows[0];
  if (!deleted) return false;

  const filenames = new Set(
    receiptRows
      .map((row) => row.receipt_image)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );
  const mediaFilenames = new Set(
    mediaRows
      .map((row) => row.file_url)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );
  const attachmentFilenames = new Set(
    attachmentRows
      .map((row) => row.file_url)
      .filter((value): value is string => Boolean(value))
      .map((value) => path.basename(value))
  );

  for (const filename of new Set([...filenames, ...mediaFilenames, ...attachmentFilenames])) {
    const candidates = [
      path.join(receiptsStorageDir, filename),
      path.join(path.dirname(receiptsStorageDir), "product-media", filename),
      path.join(path.dirname(receiptsStorageDir), "incident-attachments", filename),
    ];
    for (const candidate of candidates) {
      try {
        await fs.unlink(candidate);
      } catch (err) {
        const code = err instanceof Error && "code" in err ? (err as NodeJS.ErrnoException).code : null;
        if (code !== "ENOENT") {
          console.warn("[account/purge] file cleanup failed:", candidate, err);
        }
      }
    }
  }

  await logLifecycleEvent("warning", "Account purged", {
    userId: deleted.id,
    email: deleted.email,
    name: deleted.name,
    receiptFilesRemoved: filenames.size,
    mode: "immediate",
  });

  return true;
}

export async function cancelPendingDeletionForUser(userId: number): Promise<UserRow | null> {
  const current = await findUserById(userId);
  if (!current) return null;

  await query("DELETE FROM account_deletion_tokens WHERE user_id = $1", [userId]);

  const updatedRows = await query<UserRow>(
    `UPDATE users
     SET status = 'active',
         deletion_requested_at = NULL,
         deletion_scheduled_for = NULL
     WHERE id = $1
       AND deleted_at IS NULL
     RETURNING *`,
    [userId]
  );

  const updated = updatedRows[0] ?? null;
  if (!updated) return null;

  const body = [
    `Hola ${updated.name},`,
    "",
    "La eliminación de tu cuenta ha sido cancelada por un administrador.",
    "Tu cuenta vuelve a estar activa y puedes seguir usandola con normalidad.",
  ].join("\n");

  const outbox = await query<{ id: number }>(
    `INSERT INTO email_outbox (user_id, recipient, subject, body)
     VALUES ($1, $2, $3, $4)
     RETURNING id`,
    [
      updated.id,
      updated.email,
      "La eliminación de tu cuenta ha sido cancelada",
      body,
    ]
  );

  const outboxId = outbox[0]?.id;
  if (outboxId) {
    void sendOutboxEmail({
      id: outboxId,
      recipient: updated.email,
      subject: "La eliminación de tu cuenta ha sido cancelada",
      body,
    });
  }

  return updated;
}

export async function purgeDueAccounts(limit = 100): Promise<number> {
  const accounts = await listAccountsReadyForPurge(limit);
  let purged = 0;

  for (const account of accounts) {
    const deleted = await purgeAccount(account.id);
    if (deleted) purged += 1;
  }

  return purged;
}

export function toSafeUserAccount(user: UserRow) {
  return safeUser(user);
}

export async function exportAccountData(userId: number): Promise<Record<string, unknown>> {
  const user = await findUserById(userId);
  if (!user) {
    throw new Error("USER_NOT_FOUND");
  }

  const [notificationPreferences, pushSubscriptions, products, incidents, productMedia, incidentAttachments, legalAcceptances] = await Promise.all([
    getNotificationPreferences(userId),
    listPushSubscriptions(userId),
    query(`
      SELECT *
      FROM products
      WHERE user_id = $1
      ORDER BY created_at DESC
    `, [userId]),
    query(`
      SELECT *
      FROM repair_incidents
      WHERE user_id = $1
      ORDER BY created_at DESC
    `, [userId]),
    query(`
      SELECT *
      FROM product_media
      WHERE user_id = $1
      ORDER BY created_at DESC
    `, [userId]),
    query(`
      SELECT *
      FROM incident_attachments
      WHERE user_id = $1
      ORDER BY created_at DESC
    `, [userId]),
    getUserLegalAcceptances(userId),
  ]);

  return {
    exportedAt: new Date().toISOString(),
    user: safeUser(user),
    lifecycle: {
      status: user.status,
      lastLoginAt: user.last_login_at,
      deletionRequestedAt: user.deletion_requested_at,
      deletionScheduledFor: user.deletion_scheduled_for,
      deletedAt: user.deleted_at,
    },
    notificationPreferences,
    pushSubscriptions,
    legalAcceptances,
    products,
    productMedia,
    incidentAttachments,
    incidents,
  };
}
