import { Surreal, RecordId } from "surrealdb";
import { env } from "$env/dynamic/private";

export const SURREAL_CONNECTION_ERROR_MESSAGE =
  "Connexion à SurrealDB impossible. Vérifiez la base de données puis réessayez.";

type ConnectionErrorOptions = ErrorOptions & {
  status?: number;
  details?: string;
};

export class SurrealConnectionError extends Error {
  readonly status?: number;
  readonly details?: string;

  constructor(
    message = SURREAL_CONNECTION_ERROR_MESSAGE,
    options: ConnectionErrorOptions = {},
  ) {
    super(message, options);
    this.name = "SurrealConnectionError";
    this.status = options.status;
    this.details = options.details;
  }
}

type ConnectionKey = {
  namespace: string;
  database: string;
};

const ensureRpcUrl = (value: string) => {
  const trimmed = value.trim();
  if (!trimmed) {
    throw new Error("Missing SurrealDB URL (SURREAL_URL)");
  }
  if (trimmed.endsWith("/rpc")) {
    return trimmed;
  }
  return `${trimmed.replace(/\/$/, "")}/rpc`;
};

const resolveNamespace = () => {
  const namespace = env.SURREAL_NAMESPACE ?? env.SURREAL_NS ?? "";
  if (!namespace) {
    throw new Error("Missing SurrealDB namespace (SURREAL_NAMESPACE)");
  }
  return namespace;
};

const resolveDatabase = (database?: string | null) => {
  const candidate = String(
    database ?? env.SURREAL_DATABASE ?? env.SURREAL_DB ?? ""
  ).trim();
  if (!candidate) {
    throw new Error("Missing SurrealDB database name");
  }
  return candidate;
};

const connectionPool = new Map<string, Surreal>();
const lastActivityMap = new Map<string, number>();

const CONNECTION_TIMEOUT_MS = 10000; // 10 seconds for cloud connections
const HEALTH_CHECK_TIMEOUT_MS = 3000; // 3 seconds for health checks
const CONNECTION_MAX_IDLE_MS = 60000; // 60 seconds max idle before health check
const KEEPALIVE_INTERVAL_MS = 10000; // 10 seconds keepalive ping

// Keepalive pings to prevent idle disconnection
const keepaliveTimers = new Map<string, ReturnType<typeof setInterval>>();

function startKeepalive(key: string, client: Surreal) {
  stopKeepalive(key);
  const timer = setInterval(async () => {
    try {
      await withTimeout(client.query("SELECT 1"), HEALTH_CHECK_TIMEOUT_MS, "keepalive");
      lastActivityMap.set(key, Date.now());
    } catch {
      // Connection dead — cleanup, will reconnect on next getSurrealDB call
      connectionPool.delete(key);
      lastActivityMap.delete(key);
      stopKeepalive(key);
      client.close().catch(() => {});
    }
  }, KEEPALIVE_INTERVAL_MS);
  keepaliveTimers.set(key, timer);
}

function stopKeepalive(key: string) {
  const timer = keepaliveTimers.get(key);
  if (timer) {
    clearInterval(timer);
    keepaliveTimers.delete(key);
  }
}

const withTimeout = async <T>(
  promise: Promise<T>,
  ms: number,
  label: string
): Promise<T> => {
  return new Promise<T>((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error(`Timeout ${label} after ${ms}ms`));
    }, ms);

    promise
      .then((value) => {
        clearTimeout(timer);
        resolve(value);
      })
      .catch((error) => {
        clearTimeout(timer);
        reject(error);
      });
  });
};

const connectionKey = ({ namespace, database }: ConnectionKey) =>
  `${namespace}::${database}`;

const extractStatus = (error: unknown): number | undefined => {
  if (!error) return undefined;
  if (typeof Response !== "undefined" && error instanceof Response) {
    return error.status;
  }
  if (typeof error === "object") {
    const status =
      (error as { status?: number }).status ??
      (error as { statusCode?: number }).statusCode ??
      (error as { code?: number }).code;
    return typeof status === "number" ? status : undefined;
  }
  return undefined;
};

const extractDetails = (error: unknown): string | undefined => {
  if (!error) return undefined;
  if (typeof error === "string") return error;
  if (error instanceof Error) return error.message;
  if (typeof error === "object" && "message" in (error as object)) {
    const message = (error as { message?: unknown }).message;
    return typeof message === "string" ? message : undefined;
  }
  return undefined;
};

function toConnectionError(error: unknown): SurrealConnectionError {
  if (error instanceof SurrealConnectionError) return error;
  const status = extractStatus(error);
  const details = extractDetails(error);

  const suffix = status
    ? ` (code ${status})`
    : details
      ? ` (${details})`
      : "";

  return new SurrealConnectionError(
    `${SURREAL_CONNECTION_ERROR_MESSAGE}${suffix}`,
    {
      cause: error instanceof Error ? error : undefined,
      status,
      details,
    }
  );
}

async function createConnection(targetDatabase: string) {
  const client = new Surreal();
  try {
    await withTimeout(
      client.connect(ensureRpcUrl(env.SURREAL_URL ?? "")),
      CONNECTION_TIMEOUT_MS,
      "SurrealDB connect"
    );
    // AUTH ROOT - pas de namespace/database dans signin !
    await withTimeout(
      client.signin({
        username: env.SURREAL_USER,
        password: env.SURREAL_PASS,
      }),
      CONNECTION_TIMEOUT_MS,
      "SurrealDB signin"
    );
    await withTimeout(
      client.use({
        namespace: resolveNamespace(),
        database: targetDatabase,
      }),
      CONNECTION_TIMEOUT_MS,
      "SurrealDB use"
    );
    return client;
  } catch (error) {
    await client.close();
    throw toConnectionError(error);
  }
}

// Detect if an error is a disconnection error that warrants a retry
function isDisconnectionError(error: unknown): boolean {
  if (!error) return false;
  const message = error instanceof Error ? error.message : String(error);
  return (
    message.includes('EngineDisconnected') ||
    message.includes('connection') ||
    message.includes('disconnected') ||
    message.includes('Connection closed') ||
    message.includes('socket hang up') ||
    message.includes('ECONNRESET')
  );
}

// Force reconnection by clearing the pool entry
async function forceReconnect(database?: string | null): Promise<Surreal> {
  const namespace = resolveNamespace();
  const targetDatabase = resolveDatabase(database);
  const key = connectionKey({ namespace, database: targetDatabase });
  
  // Remove stale connection from pool
  const existing = connectionPool.get(key);
  if (existing) {
    connectionPool.delete(key);
    lastActivityMap.delete(key);
    stopKeepalive(key);
    existing.close().catch(() => {});
  }
  
  // Create fresh connection
  const client = await createConnection(targetDatabase);
  connectionPool.set(key, client);
  lastActivityMap.set(key, Date.now());
  startKeepalive(key, client);
  return client;
}

export async function getSurrealDB(
  database?: string | null
): Promise<Surreal> {
  try {
    const namespace = resolveNamespace();
    const targetDatabase = resolveDatabase(database);
    const key = connectionKey({ namespace, database: targetDatabase });

    const existing = connectionPool.get(key);
    const lastActivity = lastActivityMap.get(key) || 0;
    const now = Date.now();
    
    if (existing) {
      // Si la dernière activité est récente (keepalive maintient la connexion), pas de health check
      if (now - lastActivity < CONNECTION_MAX_IDLE_MS) {
        lastActivityMap.set(key, now);
        return existing;
      }
      
      // Vérifier si la connexion est toujours valide (avec timeout)
      try {
        await withTimeout(
          existing.query("SELECT 1"),
          HEALTH_CHECK_TIMEOUT_MS,
          "SurrealDB health check"
        );
        lastActivityMap.set(key, now);
        return existing;
      } catch (healthError) {
        // La connexion est morte, la supprimer du pool
        console.log("[SurrealDB] Connection stale, reconnecting...");
        connectionPool.delete(key);
        lastActivityMap.delete(key);
        stopKeepalive(key);
        existing.close().catch(() => {});
      }
    }

    const client = await createConnection(targetDatabase);
    connectionPool.set(key, client);
    lastActivityMap.set(key, now);
    startKeepalive(key, client);
    return client;
  } catch (error) {
    throw toConnectionError(error);
  }
}

export async function closeSurrealDB(
  database?: string | null
): Promise<void> {
  const namespace = resolveNamespace();
  if (database) {
    const targetDatabase = resolveDatabase(database);
    const key = connectionKey({ namespace, database: targetDatabase });
    const client = connectionPool.get(key);
    if (client) {
      stopKeepalive(key);
      await client.close();
      connectionPool.delete(key);
      lastActivityMap.delete(key);
    }
    return;
  }

  const keys = Array.from(connectionPool.keys()).filter((key) =>
    key.startsWith(`${namespace}::`)
  );

  await Promise.all(
    keys.map(async (key) => {
      const client = connectionPool.get(key);
      if (client) {
        stopKeepalive(key);
        await client.close();
        connectionPool.delete(key);
        lastActivityMap.delete(key);
      }
    })
  );
}

export { Surreal, RecordId } from "surrealdb";

/**
 * Construit dynamiquement les clauses SET pour SurrealDB
 * en omettant les champs avec des valeurs null/undefined
 * (car SurrealDB option<> n'accepte pas null)
 */
export function buildSetClauses(data: Record<string, unknown>): {
  setClauses: string;
  params: Record<string, unknown>;
} {
  const clauses: string[] = [];
  const params: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(data)) {
    if (value !== null && value !== undefined && value !== "") {
      clauses.push(`${key} = $${key}`);
      params[key] = value;
    }
  }

  return {
    setClauses: clauses.join(", "),
    params,
  };
}

/**
 * Construit dynamiquement les clauses SET pour UPDATE
 * Inclut tous les champs, même vides, pour permettre de les effacer avec NONE
 */
export function buildUpdateClauses(
  data: Record<string, unknown>,
  requiredFields: string[] = []
): {
  setClauses: string;
  params: Record<string, unknown>;
} {
  const clauses: string[] = [];
  const params: Record<string, unknown> = {};

  for (const [key, value] of Object.entries(data)) {
    const isRequired = requiredFields.includes(key);

    if (value !== null && value !== undefined && value !== "") {
      clauses.push(`${key} = $${key}`);
      params[key] = value;
    } else if (!isRequired) {
      // Pour les champs optionnels vides, on utilise NONE
      clauses.push(`${key} = NONE`);
    }
  }

  return {
    setClauses: clauses.join(", "),
    params,
  };
}

/**
 * Sérialise les données SurrealDB pour SvelteKit
 * (RecordId → string, Date → ISO string)
 */
export function serializeData<T>(data: T): T {
  if (data === null || data === undefined) return data;

  if (data instanceof RecordId) {
    return data.toString() as T;
  }

  if (data instanceof Date) {
    return data.toISOString() as T;
  }

  if (Array.isArray(data)) {
    return data.map((item) => serializeData(item)) as T;
  }

  if (typeof data === "object" && data !== null) {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      result[key] = serializeData(value);
    }
    return result as T;
  }

  return data;
}

/**
 * Execute a database operation with automatic retry on disconnection.
 * Useful for operations that may fail due to stale connections.
 * 
 * @param operation - A function that takes a Surreal client and returns a Promise
 * @param database - Optional database name (uses default if not specified) 
 * @param maxRetries - Maximum number of retry attempts (default: 2)
 * @returns The result of the operation
 * @throws The last error if all retries fail
 */
export async function withRetry<T>(
  operation: (db: Surreal) => Promise<T>,
  database?: string | null,
  maxRetries = 2
): Promise<T> {
  let lastError: unknown;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const db = attempt === 0 
        ? await getSurrealDB(database) 
        : await forceReconnect(database);
      return await operation(db);
    } catch (error) {
      lastError = error;
      
      // Only retry on disconnection errors
      if (!isDisconnectionError(error) || attempt === maxRetries) {
        throw error;
      }
      
      console.log(`[SurrealDB] Connection lost, retrying (attempt ${attempt + 1}/${maxRetries})...`);
    }
  }
  
  throw lastError;
}
