import {
  addErrorLog,
  debuggerLog,
  debuggerLogInit,
  deleteErrorLog,
  LogDebuggerErrorAnalyticsPayload,
} from "actions/debuggerActions";
import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants";
import { ENTITY_TYPE, Log, LogActionPayload } from "entities/AppsmithConsole";
import {
  all,
  call,
  fork,
  put,
  select,
  take,
  takeEvery,
} from "redux-saga/effects";
import { findIndex, get, isMatch, set } from "lodash";
import { getDebuggerErrors } from "selectors/debuggerSelectors";
import {
  getAction,
  getPlugin,
  getJSCollection,
} from "selectors/entitiesSelector";
import { Action, PluginType } from "entities/Action";
import { JSCollection } from "entities/JSCollection";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import {
  getDataTree,
  getEvaluationInverseDependencyMap,
} from "selectors/dataTreeSelectors";
import {
  getEntityNameAndPropertyPath,
  isAction,
  isWidget,
} from "workers/evaluationUtils";
import { getDependencyChain } from "components/editorComponents/Debugger/helpers";
import {
  ACTION_CONFIGURATION_UPDATED,
  createMessage,
  WIDGET_PROPERTIES_UPDATED,
} from "constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import { getWidget } from "./selectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Plugin } from "api/PluginApi";
import { getCurrentPageId } from "selectors/editorSelectors";
import { WidgetProps } from "widgets/BaseWidget";
import * as log from "loglevel";

// Saga to format action request values to be shown in the debugger
function* formatActionRequestSaga(
  payload: LogActionPayload,
  requestPath?: any,
) {
  // If there are no headers or body we don't format anything.
  if (!payload.source || !payload.state || !requestPath) {
    return payload;
  }

  const request = get(payload, requestPath);

  const source = payload.source;
  const action: Action | undefined = yield select(getAction, source.id);
  // Only formatting for apis and not queries
  if (action && action.pluginType === PluginType.API) {
    // Formatting api headers here
    if (request.headers) {
      let formattedHeaders = [];

      // Convert headers from Record<string, array>[] to Record<string, string>[]
      // for showing in the logs
      formattedHeaders = Object.keys(request.headers).map((key: string) => {
        const value = request.headers[key];
        return {
          [key]: value[0],
        };
      });

      set(payload, `${requestPath}.headers`, formattedHeaders);
    }

    // Formatting api body
    if (request.body) {
      let body = request.body;

      try {
        body = JSON.parse(body);
        set(payload, `${requestPath}.body`, body);
      } catch (e) {
        // Nothing to do here, we show the api body as it is if it cannot be shown as
        // an object
      }
    }

    // Return the final payload to be logged
    return payload;
  } else {
    return payload;
  }
}

function* onEntityDeleteSaga(payload: Log) {
  const source = payload.source;

  if (!source) {
    yield put(debuggerLog(payload));
    return;
  }

  const errors: Record<string, Log> = yield select(getDebuggerErrors);
  const errorIds = Object.keys(errors);

  errorIds.map((e) => {
    const includes = e.includes(source.id);

    if (includes) {
      AppsmithConsole.deleteError(e, payload.analytics);
    }
  });

  yield put(debuggerLog(payload));
}

function* logDependentEntityProperties(payload: Log) {
  const { source, state } = payload;
  if (!state || !source) return;

  yield take(ReduxActionTypes.SET_EVALUATED_TREE);
  const dataTree: DataTree = yield select(getDataTree);

  const propertyPath = `${source.name}.` + payload.source?.propertyPath;
  const inverseDependencyMap = yield select(getEvaluationInverseDependencyMap);
  const finalValue = getDependencyChain(propertyPath, inverseDependencyMap);

  yield all(
    finalValue.map((path) => {
      const entityInfo = getEntityNameAndPropertyPath(path);
      const entity = dataTree[entityInfo.entityName];
      let log = {
        ...payload,
        state: {
          [entityInfo.propertyPath]: get(dataTree, path),
        },
      };

      if (isAction(entity)) {
        log = {
          ...log,
          text: createMessage(ACTION_CONFIGURATION_UPDATED),
          source: {
            type: ENTITY_TYPE.ACTION,
            name: entityInfo.entityName,
            id: entity.actionId,
          },
        };
      } else if (isWidget(entity)) {
        log = {
          ...log,
          text: createMessage(WIDGET_PROPERTIES_UPDATED),
          source: {
            type: ENTITY_TYPE.WIDGET,
            name: entityInfo.entityName,
            id: entity.widgetId,
          },
        };
      }

      return put(debuggerLog(log));
    }),
  );
}

function* onTriggerPropertyUpdates(payload: Log) {
  const dataTree: DataTree = yield select(getDataTree);
  const source = payload.source;

  if (!source || !source.propertyPath) return;
  const widget = dataTree[source.name];
  // If property is not a trigger property we ignore
  if (!isWidget(widget) || !(source.propertyPath in widget.triggerPaths))
    return;
  // If the value of the property is empty(or set to 'No Action')
  if (widget[source.propertyPath] === "") {
    AppsmithConsole.deleteError(`${source.id}-${source.propertyPath}`);
  }
}

function* debuggerLogSaga(action: ReduxAction<Log>) {
  const { payload } = action;

  switch (payload.logType) {
    case LOG_TYPE.WIDGET_UPDATE:
      yield put(debuggerLog(payload));
      yield call(logDependentEntityProperties, payload);
      yield call(onTriggerPropertyUpdates, payload);
      return;
    case LOG_TYPE.ACTION_UPDATE:
      yield put(debuggerLog(payload));
      yield call(logDependentEntityProperties, payload);
      return;
    case LOG_TYPE.JS_ACTION_UPDATE:
      yield put(debuggerLog(payload));
      return;
    case LOG_TYPE.JS_PARSE_ERROR:
      yield put(addErrorLog(payload));
      break;
    case LOG_TYPE.JS_PARSE_SUCCESS:
      AppsmithConsole.deleteError(payload.source?.id ?? "");
      break;
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    // Fall through intentional here
    case LOG_TYPE.TRIGGER_EVAL_ERROR:
      yield put(debuggerLog(payload));
    case LOG_TYPE.EVAL_ERROR:
    case LOG_TYPE.EVAL_WARNING:
    case LOG_TYPE.WIDGET_PROPERTY_VALIDATION_ERROR:
      if (payload.source && payload.source.propertyPath) {
        if (payload.text) {
          yield put(addErrorLog(payload));
        }
      }
      break;
    case LOG_TYPE.ACTION_EXECUTION_ERROR:
      {
        const formattedLog = yield call(
          formatActionRequestSaga,
          payload,
          "state",
        );
        yield put(addErrorLog(formattedLog));
        yield put(debuggerLog(formattedLog));
      }
      break;
    case LOG_TYPE.ACTION_EXECUTION_SUCCESS:
      {
        const formattedLog = yield call(
          formatActionRequestSaga,
          payload,
          "state.request",
        );

        AppsmithConsole.deleteError(payload.source?.id ?? "");

        yield put(debuggerLog(formattedLog));
      }
      break;
    case LOG_TYPE.ENTITY_DELETED:
      yield fork(onEntityDeleteSaga, payload);
      break;
    default:
      yield put(debuggerLog(payload));
  }
}

// This saga is intended for analytics only
function* logDebuggerErrorAnalyticsSaga(
  action: ReduxAction<LogDebuggerErrorAnalyticsPayload>,
) {
  try {
    const { payload } = action;
    const currentPageId = yield select(getCurrentPageId);

    if (payload.entityType === ENTITY_TYPE.WIDGET) {
      const widget: WidgetProps | undefined = yield select(
        getWidget,
        payload.entityId,
      );
      const widgetType = widget?.type || payload?.analytics?.widgetType || "";
      const propertyPath = `${widgetType}.${payload.propertyPath}`;

      // Sending widget type for widgets
      AnalyticsUtil.logEvent(payload.eventName, {
        entityType: widgetType,
        propertyPath,
        errorMessages: payload.errorMessages,
        pageId: currentPageId,
        errorMessage: payload.errorMessage,
        errorType: payload.errorType,
      });
    } else if (payload.entityType === ENTITY_TYPE.ACTION) {
      const action: Action | undefined = yield select(
        getAction,
        payload.entityId,
      );
      const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
      const plugin: Plugin = yield select(getPlugin, pluginId);
      const pluginName = plugin.name.replace(/ /g, "");
      let propertyPath = `${pluginName}`;

      if (payload.propertyPath) {
        propertyPath += `.${payload.propertyPath}`;
      }

      // Sending plugin name for actions
      AnalyticsUtil.logEvent(payload.eventName, {
        entityType: pluginName,
        propertyPath,
        errorMessages: payload.errorMessages,
        pageId: currentPageId,
        errorMessage: payload.errorMessage,
        errorType: payload.errorType,
        errorSubType: payload.errorSubType,
      });
    } else if (payload.entityType === ENTITY_TYPE.JSACTION) {
      const action: JSCollection = yield select(
        getJSCollection,
        payload.entityId,
      );
      const plugin: Plugin = yield select(getPlugin, action.pluginId);
      const pluginName = plugin.name.replace(/ /g, "");

      // Sending plugin name for actions
      AnalyticsUtil.logEvent(payload.eventName, {
        entityType: pluginName,
        propertyPath: payload.propertyPath,
        errorMessages: payload.errorMessages,
        pageId: currentPageId,
      });
    }
  } catch (e) {
    log.error(e);
  }
}

function* addDebuggerErrorLogSaga(action: ReduxAction<Log>) {
  const payload = action.payload;
  const errors: Record<string, Log> = yield select(getDebuggerErrors);

  if (!payload.source || !payload.id) return;

  yield put(debuggerLogInit(payload));

  const analyticsPayload = {
    entityName: payload.source.name,
    entityType: payload.source.type,
    entityId: payload.source.id,
    propertyPath: payload.source.propertyPath ?? "",
  };

  // If this is a new error
  if (!(payload.id in errors)) {
    const errorMessages = payload.messages ?? [];

    yield put({
      type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
      payload: {
        ...analyticsPayload,
        eventName: "DEBUGGER_NEW_ERROR",
        errorMessages: payload.messages,
      },
    });

    // Log analytics for new error messages
    if (errorMessages.length && payload) {
      yield all(
        errorMessages.map((errorMessage) =>
          put({
            type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
            payload: {
              ...analyticsPayload,
              eventName: "DEBUGGER_NEW_ERROR_MESSAGE",
              errorMessage: errorMessage.message,
              errorType: errorMessage.type,
              errorSubType: errorMessage.subType,
            },
          }),
        ),
      );
    }
  } else {
    const updatedErrorMessages = payload.messages ?? [];
    const existingErrorMessages = errors[payload.id].messages ?? [];
    // Log new error messages
    yield all(
      updatedErrorMessages.map((updatedErrorMessage) => {
        const exists = findIndex(
          existingErrorMessages,
          (existingErrorMessage) => {
            return isMatch(existingErrorMessage, updatedErrorMessage);
          },
        );

        if (exists < 0) {
          return put({
            type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
            payload: {
              ...analyticsPayload,
              eventName: "DEBUGGER_NEW_ERROR_MESSAGE",
              errorMessage: updatedErrorMessage.message,
              errorType: updatedErrorMessage.type,
              errorSubType: updatedErrorMessage.subType,
            },
          });
        }
      }),
    );
    // Log resolved error messages
    yield all(
      existingErrorMessages.map((existingErrorMessage) => {
        const exists = findIndex(
          updatedErrorMessages,
          (updatedErrorMessage) => {
            return isMatch(updatedErrorMessage, existingErrorMessage);
          },
        );

        if (exists < 0) {
          return put({
            type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
            payload: {
              ...analyticsPayload,
              eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE",
              errorMessage: existingErrorMessage.message,
              errorType: existingErrorMessage.type,
              errorSubType: existingErrorMessage.subType,
            },
          });
        }
      }),
    );
  }
}

function* deleteDebuggerErrorLogSaga(
  action: ReduxAction<{ id: string; analytics: Log["analytics"] }>,
) {
  const errors: Record<string, Log> = yield select(getDebuggerErrors);
  // If no error exists with this id
  if (!(action.payload.id in errors)) return;

  const error = errors[action.payload.id];

  if (!error || !error.source) return;

  const analyticsPayload = {
    entityName: error.source.name,
    entityType: error.source.type,
    entityId: error.source.id,
    propertyPath: error.source.propertyPath ?? "",
    analytics: action.payload.analytics,
  };
  const errorMessages = error.messages;

  yield put({
    type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
    payload: {
      ...analyticsPayload,
      eventName: "DEBUGGER_RESOLVED_ERROR",
      errorMessages,
    },
  });

  if (errorMessages) {
    yield all(
      errorMessages.map((errorMessage) => {
        return put({
          type: ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
          payload: {
            ...analyticsPayload,
            eventName: "DEBUGGER_RESOLVED_ERROR_MESSAGE",
            errorMessage: errorMessage.message,
            errorType: errorMessage.type,
            errorSubType: errorMessage.subType,
          },
        });
      }),
    );
  }

  yield put(deleteErrorLog(action.payload.id));
}

export default function* debuggerSagasListeners() {
  yield all([
    takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga),

    takeEvery(
      ReduxActionTypes.DEBUGGER_ERROR_ANALYTICS,
      logDebuggerErrorAnalyticsSaga,
    ),
    takeEvery(
      ReduxActionTypes.DEBUGGER_ADD_ERROR_LOG_INIT,
      addDebuggerErrorLogSaga,
    ),
    takeEvery(
      ReduxActionTypes.DEBUGGER_DELETE_ERROR_LOG_INIT,
      deleteDebuggerErrorLogSaga,
    ),
  ]);
}
