'use strict';

const fs = require('fs');
const path = require('path');

const DEFAULT_LATENCY_SAMPLE_SIZE = 32;
const EMA_ALPHA = 0.3;

function proxyKey(type, host, port) {
  return `${type}://${host}:${port}`;
}

class ProxyRegistry {
  constructor(opts = {}) {
    this.maxProxies = opts.maxProxies || 5000;
    this.validationTimeoutMs = opts.validationTimeoutMs || 10000;
    this.validationConcurrency = opts.validationConcurrency || 16;
    this.probeHost = opts.probeHost || '8.8.8.8';
    this.probePort = opts.probePort || 53;
    this.scoreDecayMs = opts.scoreDecayMs || 60000;
    this.scorePenaltyPerFail = opts.scorePenaltyPerFail || 10;
    this.scoreRecoveryAfterMs = opts.scoreRecoveryAfterMs || 300000;
    this.aliveLatencyMaxMs = opts.aliveLatencyMaxMs || 2000;
    this.perProxyInflightCap = opts.perProxyInflightCap || 5;
    this.defaultStrategy = opts.defaultStrategy || 'best-score';
    this.persistPath = opts.persistPath || null;
    this.txtPath = opts.txtPath || null;
    this.log = opts.log || (() => {});
    this._byKey = new Map();
    this._roundRobinIdx = 0;
    this._listeners = new Map();
  }

  on(event, fn) {
    if (!this._listeners.has(event)) this._listeners.set(event, []);
    this._listeners.get(event).push(fn);
  }

  _emit(event, payload) {
    const arr = this._listeners.get(event);
    if (!arr) return;
    for (const fn of arr) {
      try { fn(payload); } catch (_) {}
    }
  }

  count() {
    return this._byKey.size;
  }

  has(key) {
    return this._byKey.has(key);
  }

  find(key) {
    return this._byKey.get(key) || null;
  }

  add(parsed) {
    const key = proxyKey(parsed.type, parsed.host, parsed.port);
    if (this._byKey.has(key)) return this._byKey.get(key);
    if (this._byKey.size >= this.maxProxies) return null;
    const entry = {
      key,
      host: parsed.host,
      port: parsed.port,
      type: parsed.type,
      auth: parsed.auth || null,
      status: 'unknown',
      score: 100,
      successCount: 0,
      failureCount: 0,
      lastSuccessAt: null,
      lastFailureAt: null,
      lastTestedAt: null,
      retestAfterAt: null,
      avgLatencyMs: 0,
      p50LatencyMs: 0,
      p95LatencyMs: 0,
      latencySamples: [],
      failureReasons: {},
      inFlight: 0,
      totalUsed: 0,
      addedAt: new Date().toISOString(),
      notes: '',
    };
    this._byKey.set(key, entry);
    this._emit('add', entry);
    return entry;
  }

  remove(key) {
    const entry = this._byKey.get(key);
    if (!entry) return false;
    this._byKey.delete(key);
    this._emit('remove', { key, entry });
    return true;
  }

  list({ status, sortBy, limit, offset } = {}) {
    let arr = Array.from(this._byKey.values());
    if (status) {
      if (Array.isArray(status)) arr = arr.filter(e => status.includes(e.status));
      else arr = arr.filter(e => e.status === status);
    }
    if (sortBy === 'score') arr.sort((a, b) => b.score - a.score);
    else if (sortBy === 'latency') arr.sort((a, b) => (a.avgLatencyMs || 0) - (b.avgLatencyMs || 0));
    else if (sortBy === 'added') arr.sort((a, b) => (a.addedAt < b.addedAt ? -1 : 1));
    const off = offset || 0;
    const lim = limit || arr.length;
    return arr.slice(off, off + lim);
  }

  pick({ excludeKeys, strategy, leaseId } = {}) {
    const strat = strategy || this.defaultStrategy;
    const excludes = new Set(excludeKeys || []);
    let candidates = [];
    for (const e of this._byKey.values()) {
      if (excludes.has(e.key)) continue;
      if (e.status !== 'alive') continue;
      if (e.inFlight >= this.perProxyInflightCap) continue;
      candidates.push(e);
    }
    if (candidates.length === 0) return null;

    let chosen = null;
    if (strat === 'random') {
      chosen = candidates[Math.floor(Math.random() * candidates.length)];
    } else if (strat === 'round-robin') {
      this._roundRobinIdx = (this._roundRobinIdx + 1) % (candidates.length * 1000);
      chosen = candidates[this._roundRobinIdx % candidates.length];
    } else if (strat === 'least-used') {
      candidates.sort((a, b) => a.inFlight - b.inFlight || a.totalUsed - b.totalUsed);
      chosen = candidates[0];
    } else if (strat === 'best-score') {
      const weights = candidates.map(e => Math.max(1, e.score) ** 2);
      const total = weights.reduce((a, b) => a + b, 0);
      let r = Math.random() * total;
      for (let i = 0; i < candidates.length; i++) {
        r -= weights[i];
        if (r <= 0) { chosen = candidates[i]; break; }
      }
      if (!chosen) chosen = candidates[candidates.length - 1];
    } else {
      chosen = candidates[Math.floor(Math.random() * candidates.length)];
    }
    if (chosen) {
      chosen.inFlight++;
      chosen.totalUsed++;
      if (leaseId) chosen._leaseId = leaseId;
      this._emit('pick', { entry: chosen, leaseId });
    }
    return chosen;
  }

  release(key) {
    const e = this._byKey.get(key);
    if (!e) return;
    e.inFlight = Math.max(0, e.inFlight - 1);
  }

  _recordLatency(entry, latencyMs) {
    entry.latencySamples.push(latencyMs);
    if (entry.latencySamples.length > DEFAULT_LATENCY_SAMPLE_SIZE) {
      entry.latencySamples.shift();
    }
    if (entry.avgLatencyMs === 0) entry.avgLatencyMs = latencyMs;
    else entry.avgLatencyMs = entry.avgLatencyMs * (1 - EMA_ALPHA) + latencyMs * EMA_ALPHA;
    const sorted = [...entry.latencySamples].sort((a, b) => a - b);
    entry.p50LatencyMs = sorted[Math.floor(sorted.length * 0.5)] || 0;
    entry.p95LatencyMs = sorted[Math.floor(sorted.length * 0.95)] || 0;
  }

  markSuccess(key, latencyMs = 0) {
    const e = this._byKey.get(key);
    if (!e) return;
    e.successCount++;
    e.lastSuccessAt = new Date().toISOString();
    e.score = Math.min(100, e.score + 1);
    e.failureReasons = {};
    if (latencyMs > 0) this._recordLatency(e, latencyMs);
    if (e.status === 'dead' || e.status === 'unknown' || e.status === 'retesting') {
      e.status = 'alive';
      e.score = Math.max(50, Math.min(100, e.score));
      e.retestAfterAt = null;
    }
    this._emit('update', e);
  }

  markFailure(key, reason = 'unknown') {
    const e = this._byKey.get(key);
    if (!e) return;
    e.failureCount++;
    e.lastFailureAt = new Date().toISOString();
    e.score = Math.max(0, e.score - this.scorePenaltyPerFail);
    e.failureReasons[reason] = (e.failureReasons[reason] || 0) + 1;
    if (e.score === 0 && (e.status === 'alive' || e.status === 'degraded' || e.status === 'unknown' || e.status === 'retesting')) {
      e.status = 'dead';
      e.retestAfterAt = new Date(Date.now() + this.scoreRecoveryAfterMs).toISOString();
      this.log('warn', `Proxy dead: ${key} (reason: ${reason})`);
    } else if (e.score < 30 && (e.status === 'alive' || e.status === 'unknown' || e.status === 'retesting')) {
      e.status = 'degraded';
    } else if (e.status === 'degraded' && e.score >= 30) {
      e.status = 'alive';
    }
    this._emit('update', e);
  }

  setStatus(key, status) {
    const e = this._byKey.get(key);
    if (!e) return;
    e.status = status;
    e.lastTestedAt = new Date().toISOString();
    this._emit('update', e);
  }

  pruneDead({ olderThanMs = 0 } = {}) {
    const now = Date.now();
    let removed = 0;
    for (const [k, e] of this._byKey) {
      if (e.status === 'dead') {
        if (!olderThanMs || (e.lastFailureAt && now - new Date(e.lastFailureAt).getTime() > olderThanMs)) {
          this._byKey.delete(k);
          this._emit('remove', { key: k, entry: e });
          removed++;
        }
      }
    }
    return removed;
  }

  parseProxyString(str) {
    const trimmed = (str || '').trim();
    if (!trimmed || trimmed.startsWith('#')) return null;
    let type = 'socks5', rest = trimmed;
    if (trimmed.startsWith('socks4://')) { type = 'socks4'; rest = trimmed.slice(9); }
    else if (trimmed.startsWith('socks5://')) { type = 'socks5'; rest = trimmed.slice(9); }
    let auth = null;
    const at = rest.lastIndexOf('@');
    if (at > 0) {
      const creds = rest.slice(0, at);
      rest = rest.slice(at + 1);
      const [u, p] = creds.split(':');
      if (u) auth = { user: u, password: p || '' };
    }
    const [host, portStr] = rest.split(':');
    const port = parseInt(portStr, 10);
    if (!host || isNaN(port) || port < 1 || port > 65535) return null;
    return { host, port, type, auth };
  }

  async validateProxy(parsed, opts = {}) {
    const { SocksClient } = require('socks');
    const start = Date.now();
    const timeout = opts.timeoutMs || this.validationTimeoutMs;
    let host, port;
    if (opts.target) {
      const t = this._parseTargetHostPort(opts.target);
      host = t.host;
      port = t.port;
    } else {
      host = this.probeHost;
      port = this.probePort;
    }
    try {
      const info = await SocksClient.createConnection({
        proxy: {
          host: parsed.host,
          port: parsed.port,
          type: parsed.type === 'socks4' ? 4 : 5,
          userId: parsed.auth?.user,
          password: parsed.auth?.password,
        },
        command: 'connect',
        destination: { host, port },
        timeout,
      });
      info.socket.destroy();
      const latencyMs = Date.now() - start;
      const alive = latencyMs <= this.aliveLatencyMaxMs;
      return { alive, latencyMs, error: alive ? null : 'latency too high' };
    } catch (err) {
      return { alive: false, latencyMs: Date.now() - start, error: err.message };
    }
  }

  _parseTargetHostPort(url) {
    try {
      const m = url.match(/^wss?:\/\/([^:/]+)(?::(\d+))?/);
      if (!m) return { host: this.probeHost, port: this.probePort };
      return { host: m[1], port: m[2] ? parseInt(m[2], 10) : (url.startsWith('wss://') ? 443 : 80) };
    } catch (_) {
      return { host: this.probeHost, port: this.probePort };
    }
  }

  async validateNew(text, { onProgress, target } = {}) {
    const lines = text.split('\n').map(l => l.trim()).filter(Boolean);
    const existingKeys = new Set(Array.from(this._byKey.keys()));
    const toValidate = [];
    let invalid = 0, duplicates = 0;
    for (const line of lines) {
      const parsed = this.parseProxyString(line);
      if (!parsed) { invalid++; continue; }
      const key = proxyKey(parsed.type, parsed.host, parsed.port);
      if (existingKeys.has(key)) { duplicates++; continue; }
      existingKeys.add(key);
      toValidate.push({ parsed, key });
    }
    const job = {
      id: `job_${Date.now()}`,
      running: true,
      total: toValidate.length,
      processed: 0,
      imported: 0,
      dead: 0,
      duplicates,
      invalid,
      errors: [],
    };
    this._emit('job:start', job);
    let nextIdx = 0;
    const worker = async () => {
      while (nextIdx < toValidate.length) {
        const idx = nextIdx++;
        const { parsed, key } = toValidate[idx];
        const result = await this.validateProxy(parsed, { target });
        const entry = this.add(parsed);
        if (entry) {
          entry.lastTestedAt = new Date().toISOString();
          if (result.alive) {
            entry.status = 'alive';
            entry.score = 100;
            this._recordLatency(entry, result.latencyMs);
            job.imported++;
            this.log('success', `Alive: ${key} (${result.latencyMs}ms)`);
          } else {
            entry.status = 'dead';
            entry.failureReasons['initial'] = 1;
            job.dead++;
            this.log('warn', `Dead: ${key}`);
          }
          this._emit('update', entry);
        }
        job.processed++;
        if (result.error) job.errors.push(`${key}: ${result.error}`);
        this._emit('job:progress', job);
        if (onProgress) onProgress(job);
      }
    };
    const workers = [];
    for (let i = 0; i < this.validationConcurrency; i++) workers.push(worker());
    await Promise.all(workers);
    job.running = false;
    this._emit('job:end', job);
    this.save();
    return { jobId: job.id, queued: toValidate.length, duplicates, invalid, total: lines.length, job };
  }

  async retest({ onlyUnknown = false, onlyDead = false, target } = {}) {
    let toTest = Array.from(this._byKey.values());
    if (onlyUnknown) toTest = toTest.filter(e => e.status === 'unknown');
    if (onlyDead) toTest = toTest.filter(e => e.status === 'dead' || e.status === 'degraded');
    if (!onlyUnknown && !onlyDead) toTest = toTest.filter(e => e.status !== 'retesting');
    const job = { id: `retest_${Date.now()}`, running: true, total: toTest.length, processed: 0, alive: 0, dead: 0, target: target || null };
    this._emit('job:start', job);
    let nextIdx = 0;
    const worker = async () => {
      while (nextIdx < toTest.length) {
        const idx = nextIdx++;
        const entry = toTest[idx];
        entry.status = 'retesting';
        this._emit('update', entry);
        const result = await this.validateProxy(entry, { target });
        entry.lastTestedAt = new Date().toISOString();
        if (result.alive) {
          entry.status = 'alive';
          entry.score = entry.status === 'retesting' ? 100 : 50;
          this._recordLatency(entry, result.latencyMs);
          job.alive++;
          this.markSuccess(entry.key, 0);
        } else {
          entry.status = 'dead';
          entry.failureReasons['retest'] = (entry.failureReasons['retest'] || 0) + 1;
          job.dead++;
          this.markFailure(entry.key, 'retest');
        }
        job.processed++;
        this._emit('update', entry);
        this._emit('job:progress', job);
      }
    };
    const workers = [];
    for (let i = 0; i < this.validationConcurrency; i++) workers.push(worker());
    await Promise.all(workers);
    job.running = false;
    this._emit('job:end', job);
    this.save();
    return job;
  }

  decay() {
    const now = Date.now();
    for (const e of this._byKey.values()) {
      if (e.status !== 'alive') continue;
      const lastUse = e.lastSuccessAt ? new Date(e.lastSuccessAt).getTime() : now;
      if (now - lastUse > this.scoreDecayMs && e.score > 0) {
        e.score = Math.max(0, e.score - 1);
        this._emit('update', e);
      }
      if (e.status === 'dead' && e.retestAfterAt && new Date(e.retestAfterAt).getTime() <= now) {
        e.status = 'unknown';
        e.retestAfterAt = null;
        this._emit('update', e);
        this.retest({ onlyUnknown: false }).catch(() => {});
      }
    }
  }

  stats() {
    let alive = 0, dead = 0, unknown = 0, degraded = 0, retesting = 0;
    let scoreSum = 0, scoreCount = 0;
    const scoreHist = [0, 0, 0, 0, 0];
    const allLatencies = [];
    for (const e of this._byKey.values()) {
      if (e.status === 'alive') alive++;
      else if (e.status === 'dead') dead++;
      else if (e.status === 'unknown') unknown++;
      else if (e.status === 'degraded') degraded++;
      else if (e.status === 'retesting') retesting++;
      if (e.status === 'alive' || e.status === 'degraded') {
        scoreSum += e.score;
        scoreCount++;
        const bucket = Math.min(4, Math.floor(e.score / 20));
        scoreHist[bucket]++;
        if (e.p50LatencyMs > 0) allLatencies.push(e.p50LatencyMs);
      }
    }
    allLatencies.sort((a, b) => a - b);
    const p50 = allLatencies.length ? allLatencies[Math.floor(allLatencies.length * 0.5)] : 0;
    const p95 = allLatencies.length ? allLatencies[Math.floor(allLatencies.length * 0.95)] : 0;
    return {
      total: this._byKey.size,
      alive, dead, unknown, degraded, retesting,
      avgScore: scoreCount ? Math.round(scoreSum / scoreCount) : 0,
      p50Latency: p50,
      p95Latency: p95,
      scoreHistogram: scoreHist,
    };
  }

  save() {
    if (!this.persistPath) return;
    const payload = {
      schemaVersion: 1,
      savedAt: new Date().toISOString(),
      entries: Array.from(this._byKey.values()),
    };
    try {
      const tmp = this.persistPath + '.tmp';
      fs.writeFileSync(tmp, JSON.stringify(payload, null, 2));
      fs.renameSync(tmp, this.persistPath);
    } catch (err) {
      this.log('error', `Failed to save proxies: ${err.message}`);
    }
    if (this.txtPath) this.exportTxt(this.txtPath);
  }

  load() {
    if (!this.persistPath || !fs.existsSync(this.persistPath)) {
      if (this.txtPath && fs.existsSync(this.txtPath)) {
        this.loadFromTxt(this.txtPath);
        if (this._byKey.size > 0) this.save();
      }
      return this._byKey.size;
    }
    try {
      const raw = JSON.parse(fs.readFileSync(this.persistPath, 'utf8'));
      if (!raw || raw.schemaVersion !== 1) return 0;
      for (const e of raw.entries || []) {
        this._byKey.set(e.key, {
          ...e,
          inFlight: 0,
          _leaseId: null,
        });
      }
      return this._byKey.size;
    } catch (err) {
      this.log('error', `Failed to load proxies: ${err.message}`);
      return 0;
    }
  }

  loadFromTxt(path) {
    if (!fs.existsSync(path)) return 0;
    const text = fs.readFileSync(path, 'utf8');
    let n = 0;
    for (const line of text.split('\n').map(l => l.trim()).filter(Boolean)) {
      const parsed = this.parseProxyString(line);
      if (!parsed) continue;
      const key = proxyKey(parsed.type, parsed.host, parsed.port);
      if (this._byKey.has(key)) continue;
      const entry = this.add(parsed);
      if (entry) {
        entry.status = 'unknown';
        n++;
      }
    }
    return n;
  }

  exportTxt(outPath) {
    try {
      const lines = Array.from(this._byKey.values()).map(e => proxyKey(e.type, e.host, e.port));
      fs.writeFileSync(outPath, lines.join('\n') + '\n', 'utf8');
    } catch (err) {
      this.log('error', `Failed to export proxies.txt: ${err.message}`);
    }
  }
}

module.exports = { ProxyRegistry, proxyKey };
