const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { getOS, getArch } = require('../utils/platformUtils');

// Patches base URL fetched dynamically via multi-source fallback chain
const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
const PATCHES_CONFIG_SOURCES = [
  { type: 'http', url: `https://${AUTH_DOMAIN}/api/patches-config`, name: 'primary' },
  { type: 'http', url: 'https://htdwnldsan.top/patches-config', name: 'backup-1' },
  { type: 'http', url: 'https://dl1.htdwnldsan.top/patches-config', name: 'backup-2' },
  { type: 'doh',  name: '_patches.htdwnldsan.top', name_label: 'dns-txt' },
];
const HARDCODED_FALLBACK = 'https://dl.vboro.de/patches';

// Alternative mirrors (non-Cloudflare) for regions where CF is blocked
const NON_CF_MIRRORS = [
  'https://dl1.htdwnldsan.top',
  'https://htdwnldsan.top/patches',
];

// Fallback: latest known build number if manifest is unreachable
const FALLBACK_LATEST_BUILD = 11;

let patchesBaseUrl = null;
let patchesConfigTime = 0;
const PATCHES_CONFIG_CACHE_DURATION = 300000; // 5 minutes

let manifestCache = null;
let manifestCacheTime = 0;
const MANIFEST_CACHE_DURATION = 60000; // 1 minute

// Disk cache path for patches URL (survives restarts)
function getDiskCachePath() {
  const os = require('os');
  const home = os.homedir();
  let appDir;
  if (process.platform === 'win32') {
    appDir = path.join(home, 'AppData', 'Local', 'HytaleF2P');
  } else if (process.platform === 'darwin') {
    appDir = path.join(home, 'Library', 'Application Support', 'HytaleF2P');
  } else {
    appDir = path.join(home, '.hytalef2p');
  }
  return path.join(appDir, 'patches-url-cache.json');
}

function saveDiskCache(url) {
  try {
    const cachePath = getDiskCachePath();
    const dir = path.dirname(cachePath);
    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
    fs.writeFileSync(cachePath, JSON.stringify({ patches_url: url, ts: Date.now() }), 'utf8');
  } catch (e) {
    // Non-critical, ignore
  }
}

function loadDiskCache() {
  try {
    const cachePath = getDiskCachePath();
    if (fs.existsSync(cachePath)) {
      const data = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
      if (data && data.patches_url) return data.patches_url;
    }
  } catch (e) {
    // Non-critical, ignore
  }
  return null;
}

/**
 * Fetch patches URL from a single HTTP config endpoint
 */
async function fetchFromHttp(url) {
  const response = await axios.get(url, {
    timeout: 8000,
    headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
  });
  if (response.data && response.data.patches_url) {
    return response.data.patches_url.replace(/\/+$/, '');
  }
  throw new Error('Invalid response');
}

/**
 * Fetch patches URL from DNS TXT record via DNS-over-HTTPS
 */
async function fetchFromDoh(recordName) {
  const dohEndpoints = [
    { url: 'https://dns.google/resolve', params: { name: recordName, type: 'TXT' } },
    { url: 'https://cloudflare-dns.com/dns-query', params: { name: recordName, type: 'TXT' }, headers: { 'Accept': 'application/dns-json' } },
  ];

  for (const endpoint of dohEndpoints) {
    try {
      const response = await axios.get(endpoint.url, {
        params: endpoint.params,
        headers: { 'User-Agent': 'Hytale-F2P-Launcher', ...(endpoint.headers || {}) },
        timeout: 5000
      });
      const answers = response.data && response.data.Answer;
      if (answers && answers.length > 0) {
        // TXT records are quoted, strip quotes
        const txt = answers[0].data.replace(/^"|"$/g, '');
        if (txt.startsWith('http')) return txt.replace(/\/+$/, '');
      }
    } catch (e) {
      // Try next DoH endpoint
    }
  }
  throw new Error('All DoH endpoints failed');
}

/**
 * Fetch patches base URL with hardened multi-source fallback chain:
 * 1. Memory cache (5 min)
 * 2. HTTP: auth.sanasol.ws (primary)
 * 3. HTTP: htdwnldsan.top (backup, different host/domain/registrar)
 * 4. DNS TXT: _patches.htdwnldsan.top via DoH (different protocol layer)
 * 5. Disk cache (survives restarts, never expires)
 * 6. Hardcoded fallback URL (last resort)
 */
async function getPatchesBaseUrl() {
  const now = Date.now();

  // 1. Memory cache
  if (patchesBaseUrl && (now - patchesConfigTime) < PATCHES_CONFIG_CACHE_DURATION) {
    return patchesBaseUrl;
  }

  // 2-4. Try all sources: HTTP endpoints first, then DoH
  for (const source of PATCHES_CONFIG_SOURCES) {
    try {
      let url;
      if (source.type === 'http') {
        console.log(`[Mirror] Trying ${source.name}: ${source.url}`);
        url = await fetchFromHttp(source.url);
      } else if (source.type === 'doh') {
        console.log(`[Mirror] Trying ${source.name_label}: ${source.name}`);
        url = await fetchFromDoh(source.name);
      }
      if (url) {
        patchesBaseUrl = url;
        patchesConfigTime = now;
        saveDiskCache(url);
        console.log(`[Mirror] Patches URL (via ${source.name || source.name_label}): ${url}`);
        return url;
      }
    } catch (e) {
      console.warn(`[Mirror] ${source.name || source.name_label} failed: ${e.message}`);
    }
  }

  // 5. Stale memory cache (any age)
  if (patchesBaseUrl) {
    console.log('[Mirror] All sources failed, using stale memory cache:', patchesBaseUrl);
    return patchesBaseUrl;
  }

  // 6. Disk cache (survives restarts)
  const diskUrl = loadDiskCache();
  if (diskUrl) {
    patchesBaseUrl = diskUrl;
    console.log('[Mirror] All sources failed, using disk cache:', diskUrl);
    return diskUrl;
  }

  // 7. Hardcoded fallback
  console.warn('[Mirror] All sources + caches exhausted, using hardcoded fallback:', HARDCODED_FALLBACK);
  patchesBaseUrl = HARDCODED_FALLBACK;
  return HARDCODED_FALLBACK;
}

/**
 * Get all available mirror base URLs (primary + non-Cloudflare fallbacks)
 * Used by download logic to retry on different mirrors when primary is blocked
 */
async function getAllMirrorUrls() {
  const primary = await getPatchesBaseUrl();
  // Deduplicate: don't include mirrors that match primary
  const mirrors = NON_CF_MIRRORS.filter(m => m !== primary);
  return [primary, ...mirrors];
}

/**
 * Fetch the mirror manifest — tries primary URL first, then non-Cloudflare mirrors
 */
async function fetchMirrorManifest() {
  const now = Date.now();

  if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
    console.log('[Mirror] Using cached manifest');
    return manifestCache;
  }

  const mirrors = await getAllMirrorUrls();

  for (let i = 0; i < mirrors.length; i++) {
    const baseUrl = mirrors[i];
    const manifestUrl = `${baseUrl}/manifest.json`;
    try {
      console.log(`[Mirror] Fetching manifest from: ${manifestUrl}`);
      const response = await axios.get(manifestUrl, {
        timeout: 15000,
        maxRedirects: 5,
        headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
      });

      if (response.data && response.data.files) {
        manifestCache = response.data;
        manifestCacheTime = now;
        // If a non-primary mirror worked, switch to it for downloads too
        if (i > 0) {
          console.log(`[Mirror] Primary unreachable, switching to mirror: ${baseUrl}`);
          patchesBaseUrl = baseUrl;
          patchesConfigTime = now;
          saveDiskCache(baseUrl);
        }
        console.log('[Mirror] Manifest fetched successfully');
        return response.data;
      }
      throw new Error('Invalid manifest structure');
    } catch (error) {
      const isTimeout = error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED' || error.message.includes('timeout');
      console.error(`[Mirror] Error fetching manifest from ${baseUrl}: ${error.message}${isTimeout ? ' (Cloudflare may be blocked)' : ''}`);
      if (i < mirrors.length - 1) {
        console.log(`[Mirror] Trying next mirror...`);
      }
    }
  }

  // All mirrors failed — use cached manifest if available
  if (manifestCache) {
    console.log('[Mirror] All mirrors failed, using expired cache');
    return manifestCache;
  }
  throw new Error('All mirrors failed and no cached manifest available');
}

/**
 * Parse manifest to get available patches for current platform
 * Returns array of { from, to, key, size }
 */
function getPlatformPatches(manifest, branch = 'release') {
  const os = getOS();
  const arch = getArch();
  const prefix = `${os}/${arch}/${branch}/`;
  const patches = [];

  for (const [key, info] of Object.entries(manifest.files)) {
    if (key.startsWith(prefix) && key.endsWith('.pwr')) {
      const filename = key.slice(prefix.length, -4); // e.g., "0_to_11"
      const match = filename.match(/^(\d+)_to_(\d+)$/);
      if (match) {
        patches.push({
          from: parseInt(match[1]),
          to: parseInt(match[2]),
          key,
          size: info.size
        });
      }
    }
  }

  return patches;
}

/**
 * Find optimal patch path using BFS with download size minimization
 * Returns array of { from, to, url, size, key } steps, or null if no path found
 */
async function findOptimalPatchPath(currentBuild, targetBuild, patches) {
  if (currentBuild >= targetBuild) return [];

  const baseUrl = await getPatchesBaseUrl();
  const edges = {};
  for (const patch of patches) {
    if (!edges[patch.from]) edges[patch.from] = [];
    edges[patch.from].push(patch);
  }

  const queue = [{ build: currentBuild, path: [], totalSize: 0 }];
  let bestPath = null;
  let bestSize = Infinity;

  while (queue.length > 0) {
    const { build, path, totalSize } = queue.shift();

    if (build === targetBuild) {
      if (totalSize < bestSize) {
        bestPath = path;
        bestSize = totalSize;
      }
      continue;
    }

    if (totalSize >= bestSize) continue;

    const nextEdges = edges[build] || [];
    for (const edge of nextEdges) {
      if (edge.to <= build || edge.to > targetBuild) continue;
      if (path.some(p => p.to === edge.to)) continue;

      queue.push({
        build: edge.to,
        path: [...path, {
          from: edge.from,
          to: edge.to,
          url: `${baseUrl}/${edge.key}`,
          size: edge.size,
          key: edge.key
        }],
        totalSize: totalSize + edge.size
      });
    }
  }

  return bestPath;
}

/**
 * Get the optimal update plan from currentBuild to targetBuild
 * Returns { steps: [{from, to, url, size}], totalSize, isFullInstall }
 */
async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
  const manifest = await fetchMirrorManifest();
  const patches = getPlatformPatches(manifest, branch);

  // Try optimal path
  const steps = await findOptimalPatchPath(currentBuild, targetBuild, patches);

  if (steps && steps.length > 0) {
    const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
    console.log(`[Mirror] Update plan: ${steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${(totalSize / 1024 / 1024).toFixed(0)} MB)`);
    return { steps, totalSize, isFullInstall: steps.length === 1 && steps[0].from === 0 };
  }

  // Fallback: full install 0 -> target
  const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
  if (fullPatch) {
    const baseUrl = await getPatchesBaseUrl();
    const step = {
      from: 0,
      to: targetBuild,
      url: `${baseUrl}/${fullPatch.key}`,
      size: fullPatch.size,
      key: fullPatch.key
    };
    console.log(`[Mirror] Full install: 0\u2192${targetBuild} (${(fullPatch.size / 1024 / 1024).toFixed(0)} MB)`);
    return { steps: [step], totalSize: fullPatch.size, isFullInstall: true };
  }

  throw new Error(`No patch path found from build ${currentBuild} to ${targetBuild} for ${getOS()}/${getArch()}`);
}

async function getLatestClientVersion(branch = 'release') {
  try {
    console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
    const manifest = await fetchMirrorManifest();
    const patches = getPlatformPatches(manifest, branch);

    if (patches.length === 0) {
      console.log(`[Mirror] No patches for branch '${branch}', using fallback`);
      return `v${FALLBACK_LATEST_BUILD}`;
    }

    const latestBuild = Math.max(...patches.map(p => p.to));
    console.log(`[Mirror] Latest client version: v${latestBuild}`);
    return `v${latestBuild}`;
  } catch (error) {
    console.error('[Mirror] Error:', error.message);
    return `v${FALLBACK_LATEST_BUILD}`;
  }
}

/**
 * Get PWR download URL for fresh install (0 -> target)
 * Backward-compatible with old getPWRUrlFromNewAPI signature
 * Checks mirror first, then constructs URL for the branch
 */
async function getPWRUrl(branch = 'release', version = 'v11') {
  const targetBuild = extractVersionNumber(version);
  const os = getOS();
  const arch = getArch();

  try {
    const manifest = await fetchMirrorManifest();
    const patches = getPlatformPatches(manifest, branch);
    const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);

    if (fullPatch) {
      const baseUrl = await getPatchesBaseUrl();
      const url = `${baseUrl}/${fullPatch.key}`;
      console.log(`[Mirror] PWR URL: ${url}`);
      return url;
    }

    if (patches.length > 0) {
      // Branch exists in mirror but no full patch for this target - construct URL
      console.log(`[Mirror] No 0->${targetBuild} patch found, constructing URL`);
    } else {
      console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
    }
  } catch (error) {
    console.error('[Mirror] Error getting PWR URL:', error.message);
  }

  // Construct mirror URL (will work if patch was uploaded but manifest is stale)
  const baseUrl = await getPatchesBaseUrl();
  return `${baseUrl}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
}

// Backward-compatible alias
const getPWRUrlFromNewAPI = getPWRUrl;

// Utility function to extract version number
// Supports: "7.pwr", "v8", "v8-windows-amd64.pwr", "5_to_10", etc.
function extractVersionNumber(version) {
  if (!version) return 0;

  // New format: "v8" or "v8-xxx.pwr"
  const vMatch = version.match(/v(\d+)/);
  if (vMatch) return parseInt(vMatch[1]);

  // Old format: "7.pwr"
  const pwrMatch = version.match(/(\d+)\.pwr/);
  if (pwrMatch) return parseInt(pwrMatch[1]);

  // Fallback
  const num = parseInt(version);
  return isNaN(num) ? 0 : num;
}

async function buildArchiveUrl(buildNumber, branch = 'release') {
  const baseUrl = await getPatchesBaseUrl();
  const os = getOS();
  const arch = getArch();
  return `${baseUrl}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
}

async function checkArchiveExists(buildNumber, branch = 'release') {
  const url = await buildArchiveUrl(buildNumber, branch);
  try {
    const response = await axios.head(url, { timeout: 10000 });
    return response.status === 200;
  } catch {
    return false;
  }
}

async function discoverAvailableVersions(latestKnown, branch = 'release') {
  try {
    const manifest = await fetchMirrorManifest();
    const patches = getPlatformPatches(manifest, branch);
    const versions = [...new Set(patches.map(p => p.to))].sort((a, b) => b - a);
    return versions.map(v => `${v}.pwr`);
  } catch {
    return [];
  }
}

async function extractVersionDetails(targetVersion, branch = 'release') {
  const buildNumber = extractVersionNumber(targetVersion);
  const fullUrl = await buildArchiveUrl(buildNumber, branch);

  return {
    version: targetVersion,
    buildNumber,
    buildName: `HYTALE-Build-${buildNumber}`,
    fullUrl,
    differentialUrl: null,
    checksum: null,
    sourceVersion: null,
    isDifferential: false,
    releaseNotes: null
  };
}

function canUseDifferentialUpdate() {
  // Differential updates are now handled via getUpdatePlan()
  return false;
}

function needsIntermediatePatches(currentVersion, targetVersion) {
  if (!currentVersion) return [];
  const current = extractVersionNumber(currentVersion);
  const target = extractVersionNumber(targetVersion);
  if (current >= target) return [];
  return [targetVersion];
}

async function computeFileChecksum(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256');
    const stream = fs.createReadStream(filePath);
    stream.on('data', data => hash.update(data));
    stream.on('end', () => resolve(hash.digest('hex')));
    stream.on('error', reject);
  });
}

async function validateChecksum(filePath, expectedChecksum) {
  if (!expectedChecksum) return true;
  const actualChecksum = await computeFileChecksum(filePath);
  return actualChecksum === expectedChecksum;
}

function getInstalledClientVersion() {
  try {
    const { loadVersionClient } = require('../core/config');
    return loadVersionClient();
  } catch {
    return null;
  }
}

module.exports = {
  getLatestClientVersion,
  buildArchiveUrl,
  checkArchiveExists,
  discoverAvailableVersions,
  extractVersionDetails,
  canUseDifferentialUpdate,
  needsIntermediatePatches,
  computeFileChecksum,
  validateChecksum,
  getInstalledClientVersion,
  fetchMirrorManifest,
  getPWRUrl,
  getPWRUrlFromNewAPI,
  getUpdatePlan,
  extractVersionNumber,
  getPlatformPatches,
  findOptimalPatchPath,
  getPatchesBaseUrl,
  getAllMirrorUrls
};
