import crypto from 'crypto';
import saml20 from '@boxyhq/saml20';
import axios from 'axios';

import {
  IConnectionAPIController,
  SAMLSSOConnectionWithEncodedMetadata,
  SAMLSSOConnectionWithRawMetadata,
  SAMLSSORecord,
  Storable,
  UpdateSAMLConnectionParams,
} from '../../typings';
import * as dbutils from '../../db/utils';
import {
  extractHostName,
  extractRedirectUrls,
  IndexNames,
  validateSSOConnection,
  validateRedirectUrl,
  validateTenantAndProduct,
  isLocalhost,
  validateSortOrder,
} from '../utils';
import { JacksonError } from '../error';
import { OryController } from '../../ee/ory/ory';

async function fetchMetadata(resource: string) {
  try {
    const response = await axios(resource, {
      maxContentLength: 1000000,
      maxBodyLength: 1000000,
      timeout: 8000,
    });
    return response.data;
  } catch (error: any) {
    throw new JacksonError("Couldn't fetch XML data", error.response?.status || 400);
  }
}

function validateParsedMetadata(metadata: SAMLSSORecord['idpMetadata']) {
  if (metadata.loginType !== 'idp') {
    throw new JacksonError('Please provide a metadata with IDPSSODescriptor', 400);
  }

  if (!metadata.entityID) {
    throw new JacksonError("Couldn't parse EntityID from SAML metadata", 400);
  }

  if (!metadata.sso.redirectUrl && !metadata.sso.postUrl) {
    throw new JacksonError("Couldn't find SAML bindings for POST/REDIRECT", 400);
  }
}

function validateMetadataURL(metadataUrl: string) {
  if (!isLocalhost(metadataUrl) && !metadataUrl.startsWith('https')) {
    throw new JacksonError('Metadata URL not valid, allowed ones are localhost/HTTPS URLs', 400);
  }
}

const saml = {
  create: async (
    body: SAMLSSOConnectionWithRawMetadata | SAMLSSOConnectionWithEncodedMetadata,
    connectionStore: Storable,
    oryController: OryController
  ) => {
    const {
      encodedRawMetadata,
      rawMetadata,
      defaultRedirectUrl,
      redirectUrl,
      tenant,
      product,
      name,
      label,
      description,
      metadataUrl,
      identifierFormat,
    } = body;
    const forceAuthn = body.forceAuthn == 'true' || body.forceAuthn == true;

    let connectionClientSecret: string;

    validateSSOConnection(body, 'saml');

    const redirectUrlList = extractRedirectUrls(redirectUrl);

    validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });

    validateTenantAndProduct(tenant, product);

    if ('sortOrder' in body) {
      validateSortOrder(body.sortOrder);
    }

    const record: Partial<SAMLSSORecord> = {
      defaultRedirectUrl,
      redirectUrl: redirectUrlList,
      tenant,
      product,
      name,
      label,
      description,
      clientID: '',
      clientSecret: '',
      forceAuthn,
      identifierFormat,
      metadataUrl,
      sortOrder: parseInt(body.sortOrder as any),
    };

    let metadata = rawMetadata as string;
    if (encodedRawMetadata) {
      metadata = Buffer.from(encodedRawMetadata, 'base64').toString();
    }

    metadataUrl && validateMetadataURL(metadataUrl);

    metadata = metadataUrl ? await fetchMetadata(metadataUrl) : metadata;

    const idpMetadata = (await saml20.parseMetadata(metadata, {})) as SAMLSSORecord['idpMetadata'];

    validateParsedMetadata(idpMetadata);

    // extract provider
    let providerName = extractHostName(idpMetadata.entityID);
    if (!providerName) {
      providerName = extractHostName(idpMetadata.sso.redirectUrl || idpMetadata.sso.postUrl || '');
    }

    idpMetadata.provider = providerName ? providerName : 'Unknown';

    record.clientID = dbutils.keyDigest(dbutils.keyFromParts(tenant, product, idpMetadata.entityID));

    record.idpMetadata = idpMetadata;

    const existing = (
      await connectionStore.getByIndex({
        name: IndexNames.EntityID,
        value: idpMetadata.entityID,
      })
    ).data;

    if (existing.length > 0) {
      for (let i = 0; i < existing.length; i++) {
        const samlConfig = existing[i];
        if (samlConfig.tenant !== tenant && samlConfig.product === product) {
          throw new JacksonError('EntityID already exists for different tenant/product');
        } else if (samlConfig.tenant !== tenant && samlConfig.product !== product) {
          throw new JacksonError('EntityID already exists for different tenant/product');
        } else {
          continue;
        }
      }
    }

    const exists = await connectionStore.get(record.clientID);
    const oryProjectId = exists?.ory?.projectId;
    const oryOrganizationId = exists?.ory?.organizationId;

    if (exists) {
      connectionClientSecret = exists.clientSecret;
    } else {
      connectionClientSecret = crypto.randomBytes(24).toString('hex');
    }

    record.clientSecret = connectionClientSecret;

    const oryRes = await oryController.createConnection(
      {
        sdkToken: undefined,
        projectId: oryProjectId,
        domains: body.ory?.domains,
        organizationId: oryOrganizationId,
        error: undefined,
      },
      tenant,
      product
    );
    if (oryRes) {
      record.ory = oryRes;
    }

    await connectionStore.put(
      record.clientID,
      record,
      {
        name: IndexNames.EntityID, // secondary index on entityID
        value: idpMetadata.entityID,
      },
      {
        // secondary index on tenant + product
        name: IndexNames.TenantProduct,
        value: dbutils.keyFromParts(tenant, product),
      },
      {
        // secondary index on product
        name: IndexNames.Product,
        value: product,
      }
    );

    return record as SAMLSSORecord;
  },

  update: async (
    body: UpdateSAMLConnectionParams,
    connectionStore: Storable,
    connectionsGetter: IConnectionAPIController['getConnections'],
    oryController: OryController
  ) => {
    const {
      encodedRawMetadata, // could be empty
      rawMetadata, // could be empty
      defaultRedirectUrl,
      redirectUrl,
      name,
      label,
      description,
      forceAuthn,
      metadataUrl,
      ...clientInfo
    } = body;

    if (!clientInfo?.clientID) {
      throw new JacksonError('Please provide clientID', 400);
    }

    if (!clientInfo?.clientSecret) {
      throw new JacksonError('Please provide clientSecret', 400);
    }

    if (!clientInfo?.tenant) {
      throw new JacksonError('Please provide tenant', 400);
    }

    if (!clientInfo?.product) {
      throw new JacksonError('Please provide product', 400);
    }

    if (description && description.length > 100) {
      throw new JacksonError('Description should not exceed 100 characters', 400);
    }

    if ('sortOrder' in body) {
      validateSortOrder(body.sortOrder);
    }

    const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
    validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });

    const _savedConnection = (await connectionsGetter(clientInfo))[0] as SAMLSSORecord;

    if (_savedConnection.clientSecret !== clientInfo?.clientSecret) {
      throw new JacksonError('clientSecret mismatch', 400);
    }

    let metadata = rawMetadata;
    if (encodedRawMetadata) {
      metadata = Buffer.from(encodedRawMetadata, 'base64').toString();
    }

    metadataUrl && validateMetadataURL(metadataUrl);

    metadata = metadataUrl ? await fetchMetadata(metadataUrl) : metadata;

    let newMetadata, newMetadataUrl;
    if (metadata) {
      newMetadata = await saml20.parseMetadata(metadata, {});

      validateParsedMetadata(newMetadata);

      // extract provider
      let providerName = extractHostName(newMetadata.entityID);
      if (!providerName) {
        providerName = extractHostName(newMetadata.sso.redirectUrl || newMetadata.sso.postUrl);
      }

      newMetadata.provider = providerName ? providerName : 'Unknown';
    }

    if (newMetadata) {
      // check if clientID matches with new metadata payload
      const clientID = dbutils.keyDigest(
        dbutils.keyFromParts(clientInfo.tenant, clientInfo.product, newMetadata.entityID)
      );

      if (clientID !== clientInfo?.clientID) {
        throw new JacksonError('Tenant/Product config mismatch with IdP metadata', 400);
      }

      if (metadataUrl) {
        newMetadataUrl = metadataUrl;
      }
    }

    const record: SAMLSSORecord = {
      ..._savedConnection,
      name: name || name === '' ? name : _savedConnection.name,
      label: label || label === '' ? label : _savedConnection.label,
      description: description || description === '' ? description : _savedConnection.description,
      idpMetadata: newMetadata ? newMetadata : _savedConnection.idpMetadata,
      metadataUrl: newMetadata ? newMetadataUrl : _savedConnection.metadataUrl,
      defaultRedirectUrl: defaultRedirectUrl ? defaultRedirectUrl : _savedConnection.defaultRedirectUrl,
      redirectUrl: redirectUrlList ? redirectUrlList : _savedConnection.redirectUrl,
      forceAuthn: typeof forceAuthn === 'boolean' ? forceAuthn : _savedConnection.forceAuthn,
    };

    if ('sortOrder' in body) {
      record.sortOrder = parseInt(body.sortOrder as any);
    }

    if ('deactivated' in body) {
      record['deactivated'] = body.deactivated;
    }

    if ('identifierFormat' in body) {
      record['identifierFormat'] = body.identifierFormat;
    }

    const oryRes = await oryController.updateConnection(
      {
        sdkToken: undefined,
        projectId: _savedConnection.ory?.projectId,
        domains: _savedConnection.ory?.domains,
        organizationId: _savedConnection.ory?.organizationId,
        error: undefined,
      },
      _savedConnection.tenant,
      _savedConnection.product
    );
    if (oryRes) {
      record.ory = oryRes;
    }

    await connectionStore.put(
      clientInfo?.clientID,
      record,
      {
        // secondary index on entityID
        name: IndexNames.EntityID,
        value: _savedConnection.idpMetadata.entityID,
      },
      {
        // secondary index on tenant + product
        name: IndexNames.TenantProduct,
        value: dbutils.keyFromParts(_savedConnection.tenant, _savedConnection.product),
      },
      {
        // secondary index on product
        name: IndexNames.Product,
        value: _savedConnection.product,
      }
    );

    return record;
  },
};

export default saml;
