const path = require("path");
const fs = require("fs").promises;
require("dotenv").config({
  path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const client = require("../graphql-client/graphql-client").client;
// const CalcualteAllocations = require("../cdk/cdk-calculate-allocations").default;
const CreateFortellisLogEvent = require("./fortellis-logger");
const logger = require("../utils/logger");
const { INSERT_IOEVENT } = require("../graphql-client/queries");
const uuid = require("uuid").v4;
const AxiosLib = require("axios").default;
const axios = AxiosLib.create();
const axiosCurlirize = require("axios-curlirize").default;

// Custom error class for Fortellis API errors
class FortellisApiError extends Error {
  constructor(message, details) {
    super(message);
    this.name = "FortellisApiError";
    this.reqId = details.reqId;
    this.url = details.url;
    this.apiName = details.apiName;
    this.errorData = details.errorData;
    this.errorStatus = details.errorStatus;
    this.errorStatusText = details.errorStatusText;
    this.originalError = details.originalError;
  }
}

axiosCurlirize(axios, (_result, _err) => {
  //Left intentionally blank. We don't want to console.log. We handle logging the cURL in MakeFortellisCall once completed. 
});

const getTransactionType = (jobid) => `fortellis:${jobid}`;
const defaultFortellisTTL = 60 * 60;

async function GetAuthToken() {
  //Done with Authorization Code Flow
  //https://docs.fortellis.io/docs/tutorials/solution-integration/authorization-code-flow/
  const {
    data: { access_token, expires_in, token_type }
  } = await axios.post(
    process.env.FORTELLIS_AUTH_URL,
    {},
    {
      auth: {
        username: process.env.FORTELLIS_KEY,
        password: process.env.FORTELLIS_SECRET
      },
      params: {
        grant_type: "client_credentials",
        scope: "anonymous"
      }
    }
  );
  return access_token;
}

async function FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObject }) {
  try {
    const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;

    //Get Subscription ID from Transaction Envelope
    const { SubscriptionID } = SubscriptionObject
      ? SubscriptionObject
      : await getSessionTransactionData(socket.id, getTransactionType(jobid), `txEnvelope`);
    if (!SubscriptionID) {
      throw new Error("Subscription ID not found in transaction envelope.");
    }

    //Check to See if the subscription meta is in the Redis Cache.
    const SubscriptionMetaFromCache = await getSessionTransactionData(
      socket.id,
      getTransactionType(jobid),
      FortellisCacheEnums.SubscriptionMeta
    );

    // If it is, return it.
    if (SubscriptionMetaFromCache) {
      return SubscriptionMetaFromCache;
    } else {
      const access_token = await GetAuthToken();
      const subscriptions = await axios.get(FortellisActions.GetSubscription.url, {
        headers: { Authorization: `Bearer ${access_token}` },
        logRequest: false
      });
      const SubscriptionMeta = subscriptions.data.subscriptions.find((s) => s.subscriptionId === SubscriptionID);
      if (!SubscriptionMeta) {
        throw new Error(`Subscription metadata not found for SubscriptionID: ${SubscriptionID}`);
      }
      if (setSessionTransactionData) {
        await setSessionTransactionData(
          socket.id,
          getTransactionType(jobid),
          FortellisCacheEnums.SubscriptionMeta,
          SubscriptionMeta,
          defaultFortellisTTL
        );
      }
      return SubscriptionMeta;
    }
  } catch (error) {
    CreateFortellisLogEvent(socket, "ERROR", `Error fetching subscription metadata.`, {
      error: error.message,
      stack: error.stack
    });
    throw error;
  }
}

async function GetDepartmentId({ apiName, debug = false, SubscriptionMeta, overrideDepartmentId }) {
  if (!apiName) throw new Error("apiName not provided. Unable to get department without apiName.");
  if (!SubscriptionMeta || !Array.isArray(SubscriptionMeta.apiDmsInfo)) {
    throw new Error("Subscription metadata missing apiDmsInfo.");
  }
  if (debug) {
    console.log("API Names & Departments ");
    console.log("===========");
    console.log(JSON.stringify(SubscriptionMeta.apiDmsInfo, null, 4));
    console.log("===========");
  }

  const departmentIds = SubscriptionMeta.apiDmsInfo //Get the subscription object.
    .find((info) => info.name === apiName)?.departments; //Departments are categorized by API name and have an array of departments.

  if (overrideDepartmentId) {
    return departmentIds && departmentIds.find((d) => d.id === overrideDepartmentId)?.id;
  } else {
    return departmentIds && departmentIds[0] && departmentIds[0].id; //TODO: This makes the assumption that there is only 1 department.
  }
}

//Highest level function call to make a call to fortellis. This should be the only call required, and it will handle all the logic for making the call.
async function MakeFortellisCall({
  apiName,
  url,
  headers = {},
  body = {},
  type = "post",
  debug = false,
  requestPathParams,
  requestSearchParams = [], //Array of key/value strings like [["key", "value"]]
  jobid,
  redisHelpers,
  socket,
  SubscriptionObject, //This is used because of the get make models to bypass all of the redis calls.
  overrideDepartmentId
}) {
  //const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;

  const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams });
  if (debug) console.log(`Executing ${type} to ${fullUrl}`);
  const ReqId = uuid();
  const access_token = await GetAuthToken();
  const SubscriptionMeta = await FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObject });
  const DepartmentId = await GetDepartmentId({ apiName, debug, SubscriptionMeta, overrideDepartmentId });

  if (debug) {
    console.log(
      `ReqID: ${ReqId}  |  SubscriptionID: ${SubscriptionMeta.subscriptionId}  |  DepartmentId: ${DepartmentId}`
    );
    console.log(`Body Contents: ${JSON.stringify(body, null, 4)}`);
  }

  //Write to the IOEvent Log

  await WriteToIOEventLog({
    apiName,
    fullUrl,
    logger,
    socket,
    useremail: socket.user?.email,
    type,
    bodyshopid: ""
    //bodyshopid: job.bodyshop.id,
  })

  try {
    let result;
    switch (type) {
      case "post":
      default:
        result = await axios.post(fullUrl, body, {
          headers: {
            Authorization: `Bearer ${access_token}`,
            "Subscription-Id": SubscriptionMeta.subscriptionId,
            "Request-Id": ReqId,
            "Content-Type": "application/json",
            Accept: "application/json",
            ...(DepartmentId && { "Department-Id": DepartmentId }),
            ...headers
          }
        });
        break;
      case "get":
        result = await axios.get(fullUrl, {
          headers: {
            Authorization: `Bearer ${access_token}`,
            "Subscription-Id": SubscriptionMeta.subscriptionId,
            "Request-Id": ReqId,
            Accept: "application/json",
            "Department-Id": DepartmentId,
            ...headers
          }
        });
        break;
      case "put":
        result = await axios.put(fullUrl, body, {
          headers: {
            Authorization: `Bearer ${access_token}`,
            "Subscription-Id": SubscriptionMeta.subscriptionId,
            "Request-Id": ReqId,
            Accept: "application/json",
            "Content-Type": "application/json",
            "Department-Id": DepartmentId,
            ...headers
          }
        });
        break;
    }

    if (debug) {
      console.log(`ReqID: ${ReqId} Data`);
      console.log(JSON.stringify(result.data, null, 4));
    }

    // await writeFortellisLogToFile({
    //   timestamp: new Date().toISOString(),
    //   reqId: ReqId,
    //   url: fullUrl,
    //   request:
    //   {
    //     requestcurl: result.config.curlCommand,
    //     reqid: result.config.headers["Request-Id"] || null,
    //     subscriptionId: result.config.headers["Subscription-Id"] || null,
    //     resultdata: result.data,
    //     resultStatus: result.status
    //   },
    //   user: socket?.user?.email,
    //   jobid: socket?.recordid
    // });

    logger.log(
      "fortellis-log-event-json",
      "DEBUG",
      socket?.user?.email,
      jobid,
      {
        requestcurl: result.config.curlCommand,
        reqid: result.config.headers["Request-Id"] || null,
        subscriptionId: result.config.headers["Subscription-Id"] || null,
        resultdata: result.data,
        resultStatus: result.status
      },
    );

    if (result.data.checkStatusAfterSeconds) {
      return DelayedCallback({
        delayMeta: result.data,
        access_token,
        SubscriptionID: SubscriptionMeta.subscriptionId,
        ReqId,
        departmentIds: DepartmentId,
        jobid,
        socket
      });
    }

    return result.data;
  } catch (error) {
    const errorDetails = {
      reqId: ReqId,
      url: fullUrl,
      apiName,
      errorData: error.response?.data,
      errorStatus: error.response?.status,
      errorStatusText: error.response?.statusText,
      originalError: error
    };
    // await writeFortellisLogToFile({
    //   timestamp: new Date().toISOString(),
    //   reqId: ReqId,
    //   url: fullUrl,
    //   request:
    //   {
    //     requestcurl: error.config.curlCommand,
    //     reqid: error.config.headers["Request-Id"] || null,
    //     subscriptionId: error.config.headers["Subscription-Id"] || null,
    //     resultdata: error.message,
    //     resultStatus: error.status
    //   },
    //   user: socket?.user?.email,
    //   jobid: socket?.recordid
    // });

    logger.log(
      "fortellis-log-event-error",
      "ERROR",
      socket?.user?.email,
      socket?.recordid,
      {
        requestcurl: error.config.curlCommand,
        reqid: error.config.headers["Request-Id"] || null,
        subscriptionId: error.config.headers["Subscription-Id"] || null,
        resultdata: error.message,
        resultStatus: error.status
      },
      true
    );

    throw new FortellisApiError(`Fortellis API call failed for ${apiName}: ${error.message} | ${errorDetails?.errorData?.message}`, errorDetails);
  }
}

//Some Fortellis calls return a batch result that isn't ready immediately.
//This function will check the status of the call and wait until it is ready.
//It will try 5 times before giving up.
async function DelayedCallback({ delayMeta, access_token, SubscriptionID, ReqId, departmentIds, jobid, socket }) {
  for (let index = 0; index < 5; index++) {
    await sleep(delayMeta.checkStatusAfterSeconds * 1000);
    //Check to see if the call is ready.
    const statusResult = await axios.get(delayMeta._links.status.href, {
      headers: {
        Authorization: `Bearer ${access_token}`,
        "Subscription-Id": SubscriptionID,
        "Request-Id": ReqId,
        "Department-Id": departmentIds[0].id
      }
    });

    //TODO:  Add a check if the status result is not ready, to try again.
    if (statusResult.data.status === "complete") {
      //This may have to check again if it isn't ready.
      const batchResult = await axios.get(statusResult.data._links.result.href, {
        headers: {
          Authorization: `Bearer ${access_token}`,
          "Subscription-Id": SubscriptionID,
          "Request-Id": ReqId
          //"Department-Id": departmentIds[0].id
        }
      });
      logger.log(
      "fortellis-log-event-json-DelayedCallback",
      "DEBUG",
      socket?.user?.email,
      jobid,
      {
        requestcurl: batchResult.config.curlCommand,
        reqid: batchResult.config.headers["Request-Id"] || null,
        subscriptionId: batchResult.config.headers["Subscription-Id"] || null,
        resultdata: batchResult.data,
        resultStatus: batchResult.status
      },
    );
      // await writeFortellisLogToFile({
      //   timestamp: new Date().toISOString(),
      //   reqId: ReqId,
      //   url: statusResult.data._links.result.href,
      //   request:
      //   {
      //     requestcurl: batchResult.config.curlCommand,
      //     reqid: batchResult.config.headers["Request-Id"] || null,
      //     subscriptionId: batchResult.config.headers["Subscription-Id"] || null,
      //     resultdata: batchResult.data,
      //     resultStatus: batchResult.status
      //   },
      // });
      return batchResult;
    } else {
      return "Error!!! Still need to implement batch waiting.";
    }
  }
}
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function writeFortellisLogToFile(logObject) {
  //The was only used for the certification. Commented out in case of future need. 
  try {
    const logsDir = path.join(process.cwd(), 'logs');
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const filename = `fortellis-${timestamp}.json`;
    const filepath = path.join(logsDir, filename);

    console.log(`[writeFortellisLogToFile] Writing to: ${filepath}`);
    console.log(`[writeFortellisLogToFile] process.cwd(): ${process.cwd()}`);

    // Ensure logs directory exists
    await fs.mkdir(logsDir, { recursive: true });

    // Write log object to file
    await fs.writeFile(filepath, JSON.stringify(logObject, null, 2), 'utf8');

    console.log(`Fortellis log written to: ${filepath}`,);
  } catch (err) {
    console.error('Failed to write Fortellis log to file:', err);
  }
}

async function WriteToIOEventLog({ apiName, type, fullUrl, bodyshopid, useremail, logger, socket }) {
  try {
    await client.request(INSERT_IOEVENT, {
      event: {
        operationname: `fortellis-${apiName}-${type}`,
        //time,
        //dbevent,
        env: isProduction ? "production" : "test",
        variables: { fullUrl, type, apiName },
        bodyshopid: socket.bodyshopId,
        useremail
      }
    });
  } catch (error) {
    logger.log(
      "fortellis-tracking-error",
      "ERROR",
      socket?.user?.email,
      socket?.recordid,
      {
        operationname: `fortellis-${apiName}-`,
        //time,
        //dbevent,
        env: isProduction ? "production" : "test",
        variables: {},
        bodyshopid,
        useremail,
        error: error.message,
        stack: error.stack
      },
      true
    );
  }

}

const isProduction = process.env.NODE_ENV === "production";

//Get requests should have the trailing slash as they are used that way in the calls. 
const FortellisActions = {
  GetSubscription: {
    url: isProduction
      ? "https://subscriptions.fortellis.io/v1/solution/subscriptions"
      : "https://subscriptions.fortellis.io/v1/solution/subscriptions",
    type: "get",
    apiName: "Fortellis Get Subscriptions"
  },
  QueryVehicles: {
    url: isProduction
      ? "https://api.fortellis.io/cdkdrive/service/v1/vehicles/"
      : "https://api.fortellis.io/cdk-test/cdkdrive/service/v1/vehicles/",
    type: "get",
    apiName: "Service Vehicle - Query Vehicles"
  },
  GetMakeModel: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/makemodel/v2/bulk"
      : "https://api.fortellis.io/cdk-test/drive/makemodel/v2/bulk",
    type: "get",
    apiName: "CDK Drive Get Make Model Lite"
  },
  GetVehicleId: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/vehicle-ids/" //Request path params of vins
      : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/vehicle-ids/",
    type: "get",
    apiName: "CDK Drive Post Service Vehicle"
  },
  GetVehicleById: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/" //Request path params of vehicleId
      : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
    type: "get",
    apiName: "CDK Drive Post Service Vehicle"
  },
  QueryCustomerByName: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/customerpost/v1/search"
      : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/search",
    type: "get",
    apiName: "CDK Drive Post Customer"
  },
  ReadCustomer: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/customerpost/v1/" //Customer ID is request param.
      : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/",
    type: "get",
    apiName: "CDK Drive Post Customer"
  },
  CreateCustomer: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/customerpost/v1/"
      : "https://api.fortellis.io/cdk-test/drive/customerpost/v1/",
    type: "post",
    apiName: "CDK Drive Post Customer"
  },
  InsertVehicle: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/"
      : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
    type: "post",
    apiName: "CDK Drive Post Service Vehicle"
  },
  UpdateVehicle: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/"
      : "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
    type: "put",
    apiName: "CDK Drive Post Service Vehicle"
  },
  GetCOA: {
    type: "get",
    apiName: "CDK Drive Get Chart of Accounts",
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/chartofaccounts/v2/bulk"
      : "https://api.fortellis.io/cdk-test/drive/chartofaccounts/v2/bulk",
    waitForResult: true
  },
  StartWip: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/glpost/startWIP"
      : "https://api.fortellis.io/cdk-test/drive/glpost/startWIP",
    type: "post",
    apiName: "CDK Drive Post Accounts GL"
  },
  // TranBatchWip: {
  //   url: isProduction
  //     ? "https://api.fortellis.io/cdk/drive/glpost/transBatchWIP"
  //     : "https://api.fortellis.io/cdk-test/drive/glpost/transBatchWIP",
  //   type: "post",
  //   apiName: "CDK Drive Post Accounts GL"
  // },
  PostBatchWip: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/glpost/postBatchWIP"
      : "https://api.fortellis.io/cdk-test/drive/glpost/postBatchWIP",
    type: "post",
    apiName: "CDK Drive Post Accounts GL"
  },
  DeleteTranWip: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/glpost/postWIP"
      : "https://api.fortellis.io/cdk-test/drive/glpost/postWIP",
    type: "post",
    apiName: "CDK Drive Post Accounts GL"
  },
  QueryErrorWip: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/glpost/errWIP/"
      : "https://api.fortellis.io/cdk-test/drive/glpost/errWIP/",
    type: "get",
    apiName: "CDK Drive Post Accounts GL"
  },
  ServiceHistoryInsert: {
    url: isProduction
      ? "https://api.fortellis.io/cdk/drive/post/service-vehicle-history-mgmt/v2/"
      : "https://api.fortellis.io/cdk-test/drive/post/service-vehicle-history-mgmt/v2/",
    type: "post",
    apiName: "CDK Drive Post Service Vehicle History"
  }
};

const FortellisCacheEnums = {
  txEnvelope: "txEnvelope",
  DMSBatchTxn: "DMSBatchTxn",
  SubscriptionMeta: "SubscriptionMeta",
  DepartmentId: "DepartmentId",
  JobData: "JobData",
  DMSVid: "DMSVid",
  DMSVeh: "DMSVeh",
  DMSVehCustomer: "DMSVehCustomer",
  DMSCustList: "DMSCustList",
  DMSCust: "DMSCust",
  selectedCustomerId: "selectedCustomerId",
  DMSTransHeader: "DMSTransHeader",
  transWips: "transWips",
  DmsBatchTxnPost: "DmsBatchTxnPost",
  DMSVehHistory: "DMSVehHistory"
};

function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) {
  // Ensure the base URL ends with a single "/"
  url = url.replace(/\/+$/, "/");
  const fullPath = pathParams ? `${url}${pathParams}` : url;
  const searchParams = new URLSearchParams(requestSearchParams).toString();
  const fullUrl = searchParams ? `${fullPath}?${searchParams}` : fullPath;
  return fullUrl;
}

module.exports = {
  GetAuthToken,
  FortellisCacheEnums,
  MakeFortellisCall,
  FortellisActions,
  getTransactionType,
  defaultFortellisTTL,
  FortellisApiError,
  GetDepartmentId
};
