import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from './logger.js';

import dns from 'dns';
import net from 'net';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const FEDERATION_DOMAIN = process.env.FEDERATION_DOMAIN || '';
let privateKeyPem = process.env.FEDERATION_PRIVATE_KEY || '';
let publicKeyPem = '';

// Load or generate Ed25519 keys
if (FEDERATION_DOMAIN) {
    const keyPath = process.env.FEDERATION_KEY_PATH || '/app/keys/federation_key.pem';
    const keyDir = path.dirname(keyPath);
    if (!fs.existsSync(keyDir)) {
        fs.mkdirSync(keyDir, { recursive: true });
    }

    if (!privateKeyPem && fs.existsSync(keyPath)) {
        privateKeyPem = fs.readFileSync(keyPath, 'utf8');
    }

    if (!privateKeyPem) {
        log.federation.info('Generating new Ed25519 key pair...');
        const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
            publicKeyEncoding: { type: 'spki', format: 'pem' },
            privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
        });
        privateKeyPem = privateKey;
        fs.writeFileSync(keyPath, privateKeyPem, 'utf8');
        log.federation.info(`Saved new private key to ${keyPath}`);
    }

    // Derive public key from the loaded private key
    const currentPrivateKey = crypto.createPrivateKey(privateKeyPem);
    publicKeyPem = crypto.createPublicKey(currentPrivateKey).export({ type: 'spki', format: 'pem' });
}

// Instance discovery cache (domain → { baseUrl, publicKey, cachedAt })
const discoveryCache = new Map();
const DISCOVERY_TTL_MS = 5 * 60 * 1000; // 5 minutes

/**
 * Get this instance's federation domain.
 */
export function getFederationDomain() {
    return FEDERATION_DOMAIN;
}

/**
 * Get this instance's Ed25519 public key (PEM format).
 */
export function getPublicKey() {
    return publicKeyPem;
}

/**
 * Check if federation is configured on this instance.
 */
export function isFederationEnabled() {
    return !!(FEDERATION_DOMAIN && privateKeyPem);
}

/**
 * Ed25519 sign a JSON payload.
 * @param {object} payload
 * @returns {string} base64 signature
 */
export function signPayload(payload) {
    if (!privateKeyPem) throw new Error("Federation private key not available");
    const data = Buffer.from(JSON.stringify(payload));
    return crypto.sign(null, data, privateKeyPem).toString('base64');
}

/**
 * Verify an Ed25519 signature against a JSON payload using a remote public key.
 * @param {object} payload
 * @param {string} signature base64 signature
 * @param {string} remotePublicKeyPem
 * @returns {boolean}
 */
export function verifyPayload(payload, signature, remotePublicKeyPem) {
    if (!remotePublicKeyPem || !signature) return false;
    try {
        const data = Buffer.from(JSON.stringify(payload));
        return crypto.verify(null, data, remotePublicKeyPem, Buffer.from(signature, 'base64'));
    } catch (e) {
        log.federation.error(`Signature verification error: ${e.message}`);
        return false;
    }
}

/**
 * Check if a domain resolves to a private/internal IP address (SSRF protection).
 * Blocks RFC 1918, loopback, link-local, and cloud metadata IPs.
 * @param {string} domain
 * @returns {Promise<void>} throws if domain resolves to a blocked IP
 */
async function assertPublicDomain(domain) {
  // Allow localhost only in development
  if (domain === 'localhost' || domain === '127.0.0.1' || domain === '::1') {
    if (process.env.NODE_ENV === 'production') {
      throw new Error('Federation to localhost is blocked in production');
    }
    return; // allow in dev
  }

  // If domain is a raw IP, check it directly
  if (net.isIP(domain)) {
    if (isPrivateIP(domain)) {
      throw new Error(`Federation blocked: ${domain} resolves to a private IP`);
    }
    return;
  }

  // Resolve domain and check all resulting IPs
  const { resolve4, resolve6 } = dns.promises;
  const ips = [];
  try { ips.push(...await resolve4(domain)); } catch {}
  try { ips.push(...await resolve6(domain)); } catch {}

  if (ips.length === 0) {
    throw new Error(`Federation blocked: could not resolve ${domain}`);
  }

  for (const ip of ips) {
    if (isPrivateIP(ip)) {
      throw new Error(`Federation blocked: ${domain} resolves to a private IP (${ip})`);
    }
  }
}

function isPrivateIP(ip) {
  // IPv4 private ranges
  if (/^10\./.test(ip)) return true;
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
  if (/^192\.168\./.test(ip)) return true;
  if (/^127\./.test(ip)) return true;
  if (/^0\./.test(ip)) return true;
  if (/^169\.254\./.test(ip)) return true; // link-local
  if (ip === '::1' || ip === '::' || ip.startsWith('fe80:') || ip.startsWith('fc') || ip.startsWith('fd')) return true;
  return false;
}

/**
 * Discover a remote Redlight instance's federation API base URL.
 * Fetches https://{domain}/.well-known/redlight and caches the result.
 * Includes SSRF protection: blocks private/internal IPs.
 * @param {string} domain
 * @returns {Promise<{ baseUrl: string, publicKey: string }>}
 */
export async function discoverInstance(domain) {
    // SSRF protection: validate domain doesn't resolve to internal IP
    await assertPublicDomain(domain);

    const cached = discoveryCache.get(domain);
    if (cached && (Date.now() - cached.cachedAt) < DISCOVERY_TTL_MS) {
        return cached;
    }

    const wellKnownUrl = `https://${domain}/.well-known/redlight`;
    const TIMEOUT_MS = 10_000; // 10 seconds
    try {
        let response;
        try {
            response = await fetch(wellKnownUrl, { signal: AbortSignal.timeout(TIMEOUT_MS) });
        } catch (e) {
            // HTTP fallback only allowed in development for localhost
            if (e.message.includes('fetch') && domain === 'localhost' && process.env.NODE_ENV !== 'production') {
                response = await fetch(`http://${domain}/.well-known/redlight`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
            } else throw e;
        }

        if (!response.ok) {
            throw new Error(`Discovery failed: HTTP ${response.status}`);
        }
        const data = await response.json();

        if (!data.public_key) {
            throw new Error(`Remote instance at ${domain} did not provide a public key`);
        }

        const baseUrl = `https://${domain}${data.federation_api || '/api/federation'}`;
        const result = {
            baseUrl: (domain === 'localhost' && process.env.NODE_ENV !== 'production')
                ? baseUrl.replace('https://localhost', 'http://localhost')
                : baseUrl,
            publicKey: data.public_key,
            cachedAt: Date.now(),
        };

        discoveryCache.set(domain, result);
        return result;
    } catch (error) {
        log.federation.error(`Discovery failed for ${domain}: ${error.message}`);
        throw new Error(`Could not discover Redlight instance at ${domain}: ${error.message}`);
    }
}

/**
 * Parse a federated address like "username@domain.com".
 * @param {string} address
 * @returns {{ username: string, domain: string | null }}
 */
export function parseAddress(address) {
    if (!address) return { username: address, domain: null };
    // Accept both @user@domain (Mastodon-style) and user@domain
    const normalized = address.startsWith('@') ? address.slice(1) : address;
    if (!normalized.includes('@')) {
        return { username: normalized, domain: null };
    }
    const atIndex = normalized.lastIndexOf('@');
    return {
        username: normalized.substring(0, atIndex),
        domain: normalized.substring(atIndex + 1),
    };
}
