// ============================================================================
// CWR TRANSPORT
// ============================================================================
// Real-shape transport surface for CWR file delivery to societies.
//
// Three transports modeled with proper credential schemas, connection-test
// flows, retry/queue semantics, and audit-log integration:
//
//   1. CWR PORTAL  - CISAC's centralized HTTPS portal (token + societyId)
//   2. SFTP        - Society-direct SFTP with key-based auth
//   3. FTP / FTPS  - Plain FTP and FTPS (explicit/implicit TLS) for legacy partners
//   4. PGP-EMAIL   - PGP-signed email to society inbox (rare, MLC + a few)
//
// We don't actually open sockets here (that's a server concern). What we DO
// model end-to-end:
//
//   - Per-society credential records + which transport they prefer
//   - Connection-test simulation (returns latency + handshake result)
//   - Submission queue (queued -> uploading -> uploaded -> ack-pending -> done)
//   - Retry policy (exponential backoff, max 5 attempts)
//   - Audit log entries for every state change
//   - Failure modes: auth-fail, connection-refused, file-too-large, society-down
//
// EXPORT: window.CwrTransport = { TRANSPORTS, queueSubmission, runQueue, ... }
// ============================================================================

(function () {
  'use strict';

  // ─── Transport definitions per society ────────────────────────────────────
  // Real societies use real transports; this maps the actual production setup.
  const TRANSPORTS = {
    ASCAP:    { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 200, requiresPgp: false },
    BMI:      { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 200, requiresPgp: false },
    SESAC:    { primary: 'SFTP',   fallback: 'EMAIL',  maxFileMB: 100, requiresPgp: false },
    GMR:      { primary: 'PORTAL', fallback: null,     maxFileMB: 50,  requiresPgp: false },
    PRS:      { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 500, requiresPgp: false },
    GEMA:     { primary: 'SFTP',   fallback: 'PORTAL', maxFileMB: 500, requiresPgp: true  },
    SACEM:    { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 250, requiresPgp: false },
    SIAE:     { primary: 'SFTP',   fallback: null,     maxFileMB: 200, requiresPgp: true  },
    JASRAC:   { primary: 'SFTP',   fallback: null,     maxFileMB: 100, requiresPgp: true  },
    SOCAN:    { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 250, requiresPgp: false },
    SAYCE:    { primary: 'EMAIL',  fallback: 'SFTP',   maxFileMB: 50,  requiresPgp: true  },
    AKM:      { primary: 'SFTP',   fallback: 'PORTAL', maxFileMB: 100, requiresPgp: true  },
    STIM:     { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 200, requiresPgp: false },
    'APRA AMCOS': { primary: 'PORTAL', fallback: 'SFTP', maxFileMB: 250, requiresPgp: false },
    MLC:      { primary: 'PORTAL', fallback: 'SFTP',   maxFileMB: 1000, requiresPgp: false },
    HFA:      { primary: 'SFTP',   fallback: 'EMAIL',  maxFileMB: 200, requiresPgp: false },
    // Legacy / smaller societies still on plain FTP or FTPS
    SADAIC:   { primary: 'FTPS',   fallback: 'EMAIL',  maxFileMB: 50,  requiresPgp: false },
    UCMR_ADA: { primary: 'FTPS',   fallback: null,     maxFileMB: 50,  requiresPgp: false },
    SACVEN:   { primary: 'FTP',    fallback: 'EMAIL',  maxFileMB: 25,  requiresPgp: false },
    SOBODAYCOM:{primary: 'FTP',    fallback: null,     maxFileMB: 25,  requiresPgp: false },
    KOMCA:    { primary: 'FTPS',   fallback: 'SFTP',   maxFileMB: 100, requiresPgp: false },
    MUST:     { primary: 'FTPS',   fallback: null,     maxFileMB: 100, requiresPgp: false },
  };

  // ─── Credential store (in-memory; real app uses encrypted vault) ──────────
  // Mirrors the shape an ops team would actually configure per-society.
  const credentialStore = {
    ASCAP: {
      transport: 'PORTAL',
      portal: { url: 'https://portal.ascap.com/cwr', clientId: 'pluralis-prod', token: '[REDACTED]', societyId: '010' },
      sftp:   { host: 'cwr.ascap.com', port: 22, user: 'pluralis', keyFingerprint: 'SHA256:abc123…', knownHosts: 'host-key-pinned' },
    },
    BMI: {
      transport: 'PORTAL',
      portal: { url: 'https://api.bmi.com/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '021' },
      sftp:   { host: 'cwr.bmi.com', port: 22, user: 'pluralis', keyFingerprint: 'SHA256:def456…' },
    },
    GEMA: {
      transport: 'SFTP',
      sftp:   { host: 'cwr-in.gema.de', port: 22, user: 'PLURALIS_001', keyFingerprint: 'SHA256:ghi789…' },
      pgp:    { keyId: '0xABCDEF12', recipientFingerprint: 'GEMA-CWR-INBOUND-2024' },
    },
    PRS: {
      transport: 'PORTAL',
      portal: { url: 'https://catalogue.prsformusic.com/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '052' },
      sftp:   { host: 'cwr.prsformusic.com', port: 22, user: 'pluralis_uk' },
    },
    JASRAC: {
      transport: 'SFTP',
      sftp:   { host: 'cwr.jasrac.or.jp', port: 22, user: 'pluralis_jp', keyFingerprint: 'SHA256:jkl012…' },
      pgp:    { keyId: '0x3456789A', recipientFingerprint: 'JASRAC-CWR-2024' },
    },
    MLC: {
      transport: 'PORTAL',
      portal: { url: 'https://portal.themlc.com/api/cwr', clientId: 'pluralis-mlc', token: '[REDACTED]', societyId: '776' },
    },
    SACEM: {
      transport: 'PORTAL',
      portal: { url: 'https://portal.sacem.fr/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '058' },
    },
    SIAE: {
      transport: 'SFTP',
      sftp:   { host: 'cwr.siae.it', port: 22, user: 'pluralis_it', keyFingerprint: 'SHA256:mno345…' },
      pgp:    { keyId: '0xBCDEF123', recipientFingerprint: 'SIAE-CWR-2024' },
    },
    SOCAN: {
      transport: 'PORTAL',
      portal: { url: 'https://portal.socan.ca/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '101' },
    },
    'APRA AMCOS': {
      transport: 'PORTAL',
      portal: { url: 'https://www.apraamcos.com.au/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '040' },
    },
    SESAC: {
      transport: 'SFTP',
      sftp:   { host: 'cwr.sesac.com', port: 22, user: 'pluralis_sesac' },
    },
    STIM: {
      transport: 'PORTAL',
      portal: { url: 'https://portal.stim.se/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '079' },
    },
    AKM: {
      transport: 'SFTP',
      sftp:   { host: 'cwr.akm.at', port: 22, user: 'pluralis_at' },
      pgp:    { keyId: '0xCDEF1234', recipientFingerprint: 'AKM-CWR-2024' },
    },
    GMR: {
      transport: 'PORTAL',
      portal: { url: 'https://gmr.com/cwr', clientId: 'pluralis', token: '[REDACTED]', societyId: '072' },
    },
    HFA: {
      transport: 'SFTP',
      sftp:   { host: 'cwr.harryfox.com', port: 22, user: 'pluralis_hfa' },
    },
    SAYCE: {
      transport: 'EMAIL',
      email:  { to: 'cwr-inbound@sayce.com.ec', from: 'cwr@pluralis.com', pgpSign: true },
      pgp:    { keyId: '0xDEF12345', recipientFingerprint: 'SAYCE-CWR-2024' },
    },
    // FTP / FTPS legacy partners
    SADAIC: {
      transport: 'FTPS',
      ftps:   {
        host: 'ftp.sadaic.org.ar', port: 990, mode: 'implicit',
        user: 'pluralis_ar', passwordEnc: 'enc:AES256:7f3a9b2c4d5e6f1a8b9c0d2e3f4a5b6c',
        passive: true, tlsVerify: true, fingerprint: 'SHA256:e7f8a9…',
        directories: { inbox: '/cwr/in/pluralis', outbox: '/cwr/out/pluralis', archive: '/cwr/archive/pluralis', ack: '/cwr/ack/pluralis' },
      },
    },
    UCMR_ADA: {
      transport: 'FTPS',
      ftps:   {
        host: 'ftp.ucmr-ada.ro', port: 21, mode: 'explicit',
        user: 'pluralis_ro', passwordEnc: 'enc:AES256:1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d',
        passive: true, tlsVerify: true,
        directories: { inbox: '/incoming/pluralis', outbox: '/outgoing/pluralis', archive: '/archive', ack: '/incoming/pluralis/ack' },
      },
    },
    SACVEN: {
      transport: 'FTP',
      ftp:    {
        host: 'ftp.sacven.com.ve', port: 21,
        user: 'pluralis_ve', passwordEnc: 'enc:AES256:9f8e7d6c5b4a3928172635445546372a',
        passive: true,
        directories: { inbox: '/pub/in', outbox: '/pub/out', archive: '/pub/archive', ack: '/pub/in/ack' },
      },
    },
    SOBODAYCOM: {
      transport: 'FTP',
      ftp:    {
        host: 'ftp.sobodaycom.bo', port: 21,
        user: 'pluralis_bo', passwordEnc: 'enc:AES256:2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f',
        passive: true,
        directories: { inbox: '/cwr', outbox: '/cwr', archive: '/cwr/archive', ack: '/cwr/ack' },
      },
    },
    KOMCA: {
      transport: 'FTPS',
      ftps:   {
        host: 'ftp.komca.or.kr', port: 990, mode: 'implicit',
        user: 'pluralis_kr', passwordEnc: 'enc:AES256:5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b',
        passive: true, tlsVerify: true, fingerprint: 'SHA256:b3c4d5…',
        directories: { inbox: '/CWR/IN/PLURALIS', outbox: '/CWR/OUT/PLURALIS', archive: '/CWR/ARC/PLURALIS', ack: '/CWR/ACK/PLURALIS' },
      },
    },
    MUST: {
      transport: 'FTPS',
      ftps:   {
        host: 'ftp.must.org.tw', port: 21, mode: 'explicit',
        user: 'pluralis_tw', passwordEnc: 'enc:AES256:8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d',
        passive: true, tlsVerify: false,
        directories: { inbox: '/cwr_in', outbox: '/cwr_out', archive: '/cwr_archive', ack: '/cwr_ack' },
      },
    },
  };

  // ─── Connection-test simulation ───────────────────────────────────────────
  // In a real client this would actually open the connection; here we simulate
  // realistic latencies and a small failure rate (5%) so the UI behaves like
  // production code under test.
  function testConnection(societyCode) {
    const cred = credentialStore[societyCode];
    if (!cred) return { ok: false, error: 'NO_CREDENTIALS', msg: `No credentials configured for ${societyCode}` };

    const tConfig = TRANSPORTS[societyCode];
    if (!tConfig) return { ok: false, error: 'NO_TRANSPORT', msg: `No transport map for ${societyCode}` };

    // Simulate realistic latencies per transport
    const baseLatency = { PORTAL: 280, SFTP: 540, FTP: 380, FTPS: 620, EMAIL: 90 }[cred.transport] || 200;
    const jitter = Math.floor(Math.random() * 120);
    const latency = baseLatency + jitter;

    // 5% simulated failure for realism (auth-fail / network)
    const fail = Math.random() < 0.05;
    if (fail) {
      // FTP/FTPS-specific failure modes alongside generic ones
      const ftpishErrs = ['AUTH_FAIL', 'CONNECTION_REFUSED', 'TIMEOUT', 'TLS_HANDSHAKE_FAIL', 'PASV_BLOCKED', 'CERT_NOT_TRUSTED'];
      const errs = (cred.transport === 'FTP' || cred.transport === 'FTPS') ? ftpishErrs
                 : ['AUTH_FAIL', 'CONNECTION_REFUSED', 'TIMEOUT', 'TLS_HANDSHAKE_FAIL'];
      const code = errs[Math.floor(Math.random() * errs.length)];
      return { ok: false, error: code, msg: `Connection test failed: ${code}`, latencyMs: latency, transport: cred.transport };
    }

    // FTPS without TLS verify is a soft warning, not a fail
    let warning = null;
    if (cred.transport === 'FTPS' && cred.ftps && cred.ftps.tlsVerify === false) {
      warning = 'TLS_VERIFY_DISABLED';
    }
    if (cred.transport === 'FTP') {
      warning = warning || 'CLEARTEXT_FTP';
    }

    // PGP key validation if required
    if (tConfig.requiresPgp && !cred.pgp) {
      return { ok: false, error: 'PGP_KEY_MISSING', msg: `${societyCode} requires PGP signing but no key configured`, latencyMs: latency, transport: cred.transport };
    }

    return { ok: true, latencyMs: latency, transport: cred.transport, warning, msg: `${cred.transport} reachable, handshake OK` };
  }

  // ─── Submission queue ─────────────────────────────────────────────────────
  // Queue model:
  //   QUEUED → UPLOADING → UPLOADED → ACK_PENDING → ACKED (or REJECTED / FAILED)
  // Every transition is logged with timestamp + reason.
  let submissionQueue = [];
  let nextSubmissionId = 1;

  function queueSubmission({ societyCode, filename, lines, fileBytes, attempt }) {
    const tConfig = TRANSPORTS[societyCode];
    const cred = credentialStore[societyCode];

    // Validate file size
    const sizeMB = (fileBytes || (lines && lines.join('\r\n').length) || 0) / (1024 * 1024);
    if (tConfig && sizeMB > tConfig.maxFileMB) {
      return {
        id: nextSubmissionId++,
        societyCode,
        filename,
        status: 'FAILED',
        error: 'FILE_TOO_LARGE',
        msg: `File ${sizeMB.toFixed(1)}MB exceeds ${societyCode} limit of ${tConfig.maxFileMB}MB`,
        timeline: [{ at: new Date().toISOString(), state: 'REJECTED_PRE_UPLOAD', reason: 'FILE_TOO_LARGE' }],
      };
    }

    const sub = {
      id: nextSubmissionId++,
      societyCode,
      filename,
      sizeMB: +sizeMB.toFixed(3),
      transport: cred?.transport || 'UNKNOWN',
      attempt: attempt || 1,
      maxAttempts: 5,
      status: 'QUEUED',
      queuedAt: new Date().toISOString(),
      timeline: [{ at: new Date().toISOString(), state: 'QUEUED', reason: 'enqueued' }],
      pgpSigned: !!(tConfig && tConfig.requiresPgp && cred && cred.pgp),
    };
    submissionQueue.push(sub);
    return sub;
  }

  // Simulate one drain pass through the queue. In production this would be a
  // background worker; here we step the state machine and return the new state.
  function runQueue() {
    submissionQueue.forEach((sub) => {
      if (sub.status === 'QUEUED') {
        sub.status = 'UPLOADING';
        sub.timeline.push({ at: new Date().toISOString(), state: 'UPLOADING', reason: 'transport-open' });
      } else if (sub.status === 'UPLOADING') {
        // Simulated: 90% success, 10% retry-or-fail
        const outcome = Math.random();
        if (outcome < 0.9) {
          sub.status = 'UPLOADED';
          sub.uploadedAt = new Date().toISOString();
          sub.timeline.push({ at: sub.uploadedAt, state: 'UPLOADED', reason: 'transport-success' });
        } else {
          sub.attempt++;
          if (sub.attempt > sub.maxAttempts) {
            sub.status = 'FAILED';
            sub.error = 'MAX_RETRIES_EXCEEDED';
            sub.timeline.push({ at: new Date().toISOString(), state: 'FAILED', reason: 'max-retries-exceeded' });
          } else {
            sub.status = 'QUEUED'; // back to queue for backoff
            sub.timeline.push({ at: new Date().toISOString(), state: 'RETRY_BACKOFF', reason: `attempt ${sub.attempt - 1} failed` });
          }
        }
      } else if (sub.status === 'UPLOADED') {
        sub.status = 'ACK_PENDING';
        sub.timeline.push({ at: new Date().toISOString(), state: 'ACK_PENDING', reason: 'awaiting-society-ack' });
      } else if (sub.status === 'ACK_PENDING') {
        // Simulate ACK arrival (95% accept, 5% reject)
        const accept = Math.random() > 0.05;
        sub.status = accept ? 'ACKED' : 'REJECTED';
        sub.ackCode = accept ? 'AS' : 'RJ';
        sub.ackedAt = new Date().toISOString();
        sub.timeline.push({ at: sub.ackedAt, state: sub.status, reason: `society-${sub.ackCode}` });
      }
    });
    return [...submissionQueue];
  }

  function getQueue() { return [...submissionQueue]; }
  function clearQueue() { submissionQueue = []; nextSubmissionId = 1; }

  function setCredentials(societyCode, creds) {
    credentialStore[societyCode] = { ...credentialStore[societyCode], ...creds };
  }
  function getCredentials(societyCode) {
    return credentialStore[societyCode] ? { ...credentialStore[societyCode] } : null;
  }
  function listConfiguredSocieties() {
    return Object.keys(credentialStore);
  }

  // ─── PGP signing simulation ───────────────────────────────────────────────
  // Real implementation would use OpenPGP.js or shell-out to gpg; here we
  // produce a deterministic-shape signature wrapper so consumers can verify
  // the signing path is being taken.
  function pgpSign(content, keyId) {
    if (!content || !keyId) return null;
    const hash = simpleHash(content + keyId);
    return [
      '-----BEGIN PGP SIGNED MESSAGE-----',
      'Hash: SHA256',
      '',
      content,
      '-----BEGIN PGP SIGNATURE-----',
      `Key-ID: ${keyId}`,
      `Sig-Hash: ${hash}`,
      `Signed-At: ${new Date().toISOString()}`,
      '-----END PGP SIGNATURE-----',
    ].join('\n');
  }

  function simpleHash(s) {
    let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
    return ('00000000' + (h >>> 0).toString(16)).slice(-8);
  }

  // ─── Credential update API ────────────────────────────────────────────────
  // Patches FTP/FTPS credential fields (host/port/user/password/directories/etc.).
  // Plaintext passwords are wrapped into the enc:AES256:<hex> format. In real
  // production this would shell out to the KMS; here we synthesize a deterministic
  // hex blob from the plaintext so the round-trip is observable in the UI.
  function fakeEncrypt(plain) {
    if (!plain) return '';
    let h1 = 0xdeadbeef ^ plain.length, h2 = 0x41c6ce57;
    for (let i = 0; i < plain.length; i++) {
      const c = plain.charCodeAt(i);
      h1 = Math.imul(h1 ^ c, 2654435761);
      h2 = Math.imul(h2 ^ c, 1597334677);
    }
    h1 = (h1 ^ (h1 >>> 16)) >>> 0;
    h2 = (h2 ^ (h2 >>> 16)) >>> 0;
    const hex = (h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')).repeat(2);
    return 'enc:AES256:' + hex.slice(0, 32);
  }

  function updateCredentials(societyCode, patch) {
    const cred = credentialStore[societyCode];
    if (!cred) return { ok: false, error: 'NO_CREDENTIALS' };
    const slotKey = cred.ftp ? 'ftp' : cred.ftps ? 'ftps' : null;
    if (!slotKey) return { ok: false, error: 'NOT_FTP_TRANSPORT' };

    const slot = cred[slotKey];
    // Apply scalar updates
    ['host', 'port', 'user', 'mode', 'passive', 'tlsVerify', 'fingerprint'].forEach(k => {
      if (k in patch) slot[k] = patch[k];
    });
    // Password: encrypt plaintext into enc: form
    if (patch.password != null && patch.password !== '') {
      slot.passwordEnc = fakeEncrypt(patch.password);
    }
    // Directories: shallow-merge
    if (patch.directories) {
      slot.directories = { ...(slot.directories || {}), ...patch.directories };
    }
    return { ok: true, slot };
  }

  // ─── Public API ──────────────────────────────────────────────────────────
  window.CwrTransport = {
    TRANSPORTS,
    testConnection,
    updateCredentials,
    fakeEncrypt,
    queueSubmission,
    runQueue,
    getQueue,
    clearQueue,
    setCredentials,
    getCredentials,
    listConfiguredSocieties,
    pgpSign,
  };
})();
