const { SignedXml } = require('xml-crypto');
const { DOMParser } = require('@xmldom/xmldom');
const crypto = require('crypto');
const express = require('express');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');
const zlib = require('zlib');
const xml2js = require('xml2js');
const xpath = require('xpath');

const app = express();
const port = 3000;

// Enable body parsing
app.use(bodyParser.urlencoded({ extended: true }));

let storedSamlRequest = null;
let storedRelayState = null;
let storedQueryParameters = null;

// Step 1: Receive SAMLRequest and render login form
app.get('/saml/sso', async (req, res) => {
  storedQueryParameters = req.query;
  console.log('Received query parameters:', req.query); // 👈 log all query parameters

  const samlRequest = req.query.SAMLRequest;
  const relayState = req.query.RelayState || '';

  if (!samlRequest) {
    return res.status(400).send('Missing SAMLRequest');
  }

  // Store temporarily
  storedSamlRequest = samlRequest;
  storedRelayState = relayState;

  const authnRequestId = await extractAuthnRequestId(storedSamlRequest);
  console.log('SAML2 AuthnRequestId:', authnRequestId); 

  // Show simple login form
  res.send(createLoginForm());
});

// Step 2: Handle login form and send SAMLResponse
app.post('/saml/login', async (req, res) => {
  const { username, email, orgCode, securityGroup, roles } = req.body;
  if (!storedSamlRequest) {
    return res.status(400).send('No SAMLRequest stored');
  }
  
  // Get the issuer URL (the IDP ID [the application.yml = resilient.security.saml2.relyingparty.registration.mock-idp.assertingparty.entity-id])
  const issuerUrlDefault = 'http://localhost:3000/saml/metadata';
  const issuerUrl = storedQueryParameters['issuerUrl'] ? storedQueryParameters['issuerUrl'] : issuerUrlDefault;

  // Get the base url of ServiceProvider (the server APP)
  const serviceProviderUrlDefault = 'https://localhost:8443'; // Or 'http://localhost:8081'
  const serviceProviderUrl = storedQueryParameters['spUrl'] ? storedQueryParameters['spUrl'] : serviceProviderUrlDefault;

  // Build ACS (Assertion Consumer Service) URL — this is the Spring Boot app’s endpoint where the SAML Response is posted back after authentication.
  const acsUrlPath = '/login/saml2/sso/mock-idp';
  const acsUrl = serviceProviderUrl + acsUrlPath;

  console.log('ServiceProvider URL :', serviceProviderUrl); 
  console.log('ServiceProvider ACS :', acsUrl); 

  const inResponseTo = await extractAuthnRequestId(storedSamlRequest);
  const samlResponse = createFakeSamlResponse(username, email, orgCode, securityGroup, roles, serviceProviderUrl, issuerUrl, inResponseTo);
  const relayState = storedRelayState;

  res.send(`
    <html>
      <body onload="document.forms[0].submit()">
        <form method="POST" action="${acsUrl}">
          <input type="hidden" name="SAMLResponse" value="${samlResponse.replace(/"/g, '&quot;')}" />
          <input type="hidden" name="RelayState" value="${relayState.replace(/"/g, '&quot;')}" />
        </form>
      </body>
    </html>
  `);
});

/**
 * Extracts the AuthnRequest ID from a base64-encoded SAMLRequest.
 * @param {string} samlRequest - Base64-encoded SAMLRequest parameter from the URL.
 * @returns {Promise<string|null>} - Promise resolving to the AuthnRequest ID, or null if not found.
 */
function extractAuthnRequestId(samlRequest) {
  return new Promise((resolve, reject) => {
    try {
      const decoded = Buffer.from(samlRequest, 'base64');

      zlib.inflateRaw(decoded, (err, inflated) => {
        if (err) return reject(new Error('Failed to inflate SAMLRequest: ' + err.message));

        const xml = inflated.toString('utf8');
        const doc = new DOMParser().parseFromString(xml, 'text/xml');

        // Define namespace mappings for XPath
        const select = xpath.useNamespaces({
          saml2p: 'urn:oasis:names:tc:SAML:2.0:protocol',
        });

        const node = select('//saml2p:AuthnRequest', doc)[0];
        if (node) {
          resolve(node.getAttribute('ID'));
        } else {
          resolve(null);
        }
      });
    } catch (error) {
      reject(error);
    }
  });
}

function createFakeSamlResponse(nameId, email, orgCode, securityGroupCode, roles, serviceProviderUrl, issuerUrl, inResponseTo) {
  const issuer = issuerUrl;
  // const audience = "https://localhost:8443/saml2/service-provider-metadata/mock-idp";
  const audience = serviceProviderUrl + "/saml2/service-provider-metadata/mock-idp";
  const now = new Date().toISOString();
  const fiveMinutesLater = new Date(Date.now() + 5 * 60000).toISOString();
  const assertionId = `_assertion_${crypto.randomUUID()}`;
  const responseId = `_response_${crypto.randomUUID()}`;

  // const recipient = "https://localhost:8443/login/saml2/sso/mock-idp";
  const recipient = serviceProviderUrl + "/login/saml2/sso/mock-idp";
  // const destination = "https://localhost:8443/login/saml2/sso/mock-idp";
  const destination = serviceProviderUrl + "/login/saml2/sso/mock-idp";

  console.log('SAML Recipient   :', recipient); 
  console.log('SAML Destination :', destination); 
  console.log('SAML Issuer :', issuer); 

  const assertion = `
  <saml:Assertion 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" 
    ID="${assertionId}" 
    IssueInstant="${now}" 
    Version="2.0">
    <saml:Issuer>${issuer}</saml:Issuer>
    <saml:Subject>
      <saml:NameID>${nameId}</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="${fiveMinutesLater}" Recipient="${recipient}" />
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="${now}" NotOnOrAfter="${fiveMinutesLater}">
      <saml:AudienceRestriction>
        <saml:Audience>${audience}</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="${now}">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="email">
        <saml:AttributeValue>${email}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="organization_code">
        <saml:AttributeValue>${orgCode}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="security_group">
        <saml:AttributeValue>${securityGroupCode}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="roles">
        <saml:AttributeValue>${roles}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="urn:mace:dir:attribute-def:displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">Pedro Reis</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="urn:oid:2.16.840.1.113730.3.1.241" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">Pedro Reis</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="urn:mace:dir:attribute-def:mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">pedro.reis@unl.pt</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
        <saml:AttributeValue xsi:type="xs:string">pedro.reis@unl.pt</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>`;

  const privateKey = fs.readFileSync(path.join(__dirname, 'certs', 'idp-private.key'), 'utf8');
  const certificate = fs.readFileSync(path.join(__dirname, 'certs', 'idp-public.cert'), 'utf8');
  const certBase64 = certificate
    .replace(/-----BEGIN CERTIFICATE-----/, '')
    .replace(/-----END CERTIFICATE-----/, '')
    .replace(/\r?\n|\r/g, '');

  const doc = new DOMParser().parseFromString(assertion);
  const sig = new SignedXml();
  sig.prefix = 'ds';
  sig.addReference("//*[local-name(.)='Assertion']", [
    "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
    "http://www.w3.org/2001/10/xml-exc-c14n#",
  ], "http://www.w3.org/2000/09/xmldsig#sha1");
  sig.signingKey = privateKey;
  sig.keyInfoProvider = {
    getKeyInfo: () => `<ds:X509Data><ds:X509Certificate>${certBase64}</ds:X509Certificate></ds:X509Data>`
  };
  sig.computeSignature(assertion, {
    prefix: 'ds',
    location: { reference: "//*[local-name(.)='Issuer']", action: 'after' }
  });

  const signedAssertion = sig.getSignedXml();
  const samlResponse = `<?xml version="1.0" encoding="UTF-8"?><samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="${responseId}" Version="2.0" IssueInstant="${now}" Destination="${destination}" InResponseTo="${inResponseTo}"><saml:Issuer>${issuer}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>${signedAssertion}</samlp:Response>`;

  const samlResponseToUse = samlResponse.trim().replace(/^\uFEFF/, '');
  return Buffer.from(samlResponseToUse, 'utf-8').toString('base64');
}

// OLD assertion string. This contains also some extra attributes
function assertionOLD() {
  const assertion = `
  <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="${assertionId}" IssueInstant="${now}" Version="2.0">
    <saml:Issuer>${issuer}</saml:Issuer>
    <saml:Subject>
      <saml:NameID>${nameId}</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="${fiveMinutesLater}" Recipient="${recipient}" />
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="${now}" NotOnOrAfter="${fiveMinutesLater}">
      <saml:AudienceRestriction>
        <saml:Audience>${audience}</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="${now}">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="email">
        <saml:AttributeValue>${email}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="organization_code">
        <saml:AttributeValue>${orgCode}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="security_group">
        <saml:AttributeValue>${securityGroupCode}</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="roles">
        <saml:AttributeValue>${roles}</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>`;
}

function createLoginFormOLD() {
  const form = `
    <html>
      <body>
        <h2>Mock Login</h2>
        <form method="POST" action="/saml/login">
          Username: <input name="username" value="test-user"/><br/>
          Email: <input name="email" value="test@example.com"/><br/>
          Org Code: <input name="orgCode" value="NOVA"/><br/>
          Security Group: <input name="securityGroup" value="GRP_USER"/><br/>
          Roles: <select name="roles" multiple size="4">
            <option value="ROLE_USER">ROLE_USER</option>
            <option value="ROLE_COORDINATOR">ROLE_COORDINATOR</option>
            <option value="ROLE_MANAGER">ROLE_MANAGER</option>
            <option value="ROLE_ADMIN">ROLE_ADMIN</option>
          </select><br/><br/>
          <button type="submit">Login</button>
        </form>
      </body>
    </html>
  `;
  return form;
}

function createLoginForm() {
  const form = `
  <html>
    <head>
      <style>
        body {
          font-family: Arial, sans-serif;
          background-color: #f4f4f4;
          display: flex;
          justify-content: center;
          align-items: center;
          height: 100vh;
        }
        .form-container {
          background: #fff;
          padding: 30px 40px;
          border-radius: 8px;
          box-shadow: 0 0 10px rgba(0,0,0,0.1);
          width: 400px;
        }
        h2 {
          text-align: center;
          margin-bottom: 20px;
        }
        label {
          display: block;
          margin: 10px 0 5px;
          font-weight: bold;
        }
        input[type="text"],
        input[type="email"],
        select {
          width: 100%;
          padding: 8px;
          box-sizing: border-box;
          border-radius: 4px;
          border: 1px solid #ccc;
        }
        button {
          margin-top: 20px;
          width: 100%;
          padding: 10px;
          background-color: #007bff;
          color: white;
          border: none;
          border-radius: 4px;
          font-size: 16px;
          cursor: pointer;
        }
        button:hover {
          background-color: #0056b3;
        }
      </style>
    </head>
    <body>
      <div class="form-container">
        <h2>Mock Login</h2>
        <form method="POST" action="/saml/login">
          <label for="username">Username</label>
          <input id="username" name="username" value="test-user" type="text"/>

          <label for="email">Email</label>
          <input id="email" name="email" value="test@example.com" type="email"/>

          <label for="orgCode">Org Code</label>
          <input id="orgCode" name="orgCode" value="NOVA" type="text"/>

          <label for="securityGroup">Security Group</label>
          <input id="securityGroup" name="securityGroup" value="GRP_USER" type="text"/>

          <label for="roles">Roles</label>
          <select id="roles" name="roles" multiple size="4">
            <option value="ROLE_USER">ROLE_USER</option>
            <option value="ROLE_COORDINATOR">ROLE_COORDINATOR</option>
            <option value="ROLE_MANAGER">ROLE_MANAGER</option>
            <option value="ROLE_ADMIN">ROLE_ADMIN</option>
          </select>

          <button type="submit">Login</button>
        </form>
      </div>
    </body>
  </html>
  `;

  
  return form;
}

// Function to extract Issuer from SAMLRequest (returns a Promise)
function extractIssuerFromSAMLRequest(samlRequestBase64) {
  return new Promise((resolve, reject) => {
    const samlRequestBuffer = Buffer.from(samlRequestBase64, 'base64');

    zlib.inflateRaw(samlRequestBuffer, (err, decoded) => {
      if (err) {
        return reject(new Error('Failed to inflate SAMLRequest'));
      }

      const xml = decoded.toString('utf8');

      xml2js.parseString(xml, { tagNameProcessors: [xml2js.processors.stripPrefix] }, (parseErr, result) => {
        if (parseErr) {
          return reject(new Error('Failed to parse SAMLRequest XML'));
        }

        // Debugging: log the entire parsed result to see structure
        // console.log("Parsed XML result:", result);

        // Extract Issuer from the first element of the array
        const issuerObject = result?.AuthnRequest?.Issuer?.[0]; // Issuer is an array, so we access the first element
        if (!issuerObject) {
          return reject(new Error('Issuer not found in SAMLRequest'));
        }

        // Access the text content of the Issuer object (it may be in the '#text' property)
        const issuer = issuerObject._ || issuerObject['#text']; // Extract the actual string value

        if (!issuer) {
          return reject(new Error('Issuer value is missing in the SAMLRequest'));
        }

        resolve(issuer);  // Return the issuer as a string
      });
    });
  });
}

app.listen(port, () => {
  const now = new Date();
  const localISOTime = now.toLocaleString('pt-PT'); // ISO-like, local time

  const nowUTC = new Date().toISOString();

  console.log(`Mock IDP running at http://localhost:${port}`);
  console.log(`Started at ${localISOTime} (UTC is ${now})`);
});
