// because graphql args are sometimes imported wiht _, we use
// lodash as opposed to _ to avoid overloading
import lodash from 'lodash';

import { gatherSignalsFromResult } from '../../services/analyticsQueries/index.js';
import { jsonStringify } from '../../utils/encoding.js';
import { isCoopErrorOfType, makeNotFoundError } from '../../utils/errors.js';
import type {
  GQLQueryResolvers,
  GQLReportingRuleExecutionResultResolvers,
  GQLReportingRuleInsightsResolvers,
  GQLRuleExecutionResultResolvers,
  GQLRuleInsightsResolvers,
} from '../generated.js';
import { unauthenticatedError } from '../utils/errors.js';
import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js';

const typeDefs = /* GraphQL */ `
  enum LookbackVersion {
    LATEST
    PRIOR
  }

  type SignalWithScore {
    signalName: String!
    integration: String
    subcategory: String
    score: String!
  }

  # Not all fields are required here because, sometimes, what the backend exposes
  # to GQL to be returned as this type does not contain all the fields.
  type RuleExecutionResult {
    date: Date!
    ts: DateTime!
    contentId: String!
    itemTypeName: String!
    itemTypeId: ID!
    userId: String
    userTypeId: String
    content: String!
    result: ConditionSetWithResult
    signalResults: [SignalWithScore!]
    environment: RuleEnvironment!
    passed: Boolean!
    ruleId: ID!
    ruleName: String!
    policies: [String!]!
    tags: [String!]!
  }

  type ReportingRuleExecutionResult {
    date: Date!
    ts: DateTime!
    itemId: ID!
    itemTypeName: String!
    itemTypeId: ID!
    itemData: String!
    creatorId: String
    creatorTypeId: String
    result: ConditionSetWithResult
    signalResults: [SignalWithScore!]
    environment: RuleEnvironment!
    passed: Boolean!
    ruleId: ID!
    ruleName: String!
    policyIds: [String!]!
  }

  type RuleInsights {
    passRateData(lookbackStartDate: Date): [RulePassRateData]
    latestVersionSamples: [RuleExecutionResult!]!
    priorVersionSamples: [RuleExecutionResult!]!
  }

  type RulePassRateData {
    date: String!
    totalMatches: Float!
    totalRequests: Float!
  }

  type ReportingRuleInsights {
    passRateData(lookbackStartDate: Date): [ReportingRulePassRateData!]!
    latestVersionSamples: [ReportingRuleExecutionResult!]!
    priorVersionSamples: [ReportingRuleExecutionResult!]!
  }

  type ReportingRulePassRateData {
    date: String!
    totalMatches: Float!
    totalRequests: Float!
  }

  type UserStrikeBucket {
    numStrikes: Int!
    numUsers: Int!
  }

  input GetFullResultForItemInput {
    ruleId: ID!
    item: ItemIdentifierInput!
    date: String
    lookback: LookbackVersion
  }

  type Query {
    getUserStrikeCountDistribution: [UserStrikeBucket!]!
    getFullRuleResultForItem(
      input: GetFullResultForItemInput!
    ): GetFullResultForItemResponse!
    getFullReportingRuleResultForItem(
      input: GetFullResultForItemInput!
    ): GetFullReportingRuleResultForItemResponse!
  }

  union GetFullResultForItemResponse = RuleExecutionResult | NotFoundError
  union GetFullReportingRuleResultForItemResponse =
    | ReportingRuleExecutionResult
    | NotFoundError
`;

const RuleExecutionResult: GQLRuleExecutionResultResolvers = {
  async signalResults(ruleExecutionResult, _, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const fullResults = ruleExecutionResult.result;
    if (!fullResults) {
      return [];
    }

    return gatherSignalsFromResult(fullResults);
  },
};

const ReportingRuleExecutionResult: GQLReportingRuleExecutionResultResolvers = {
  async signalResults(reportingRuleExecutionResult, _, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const fullResults = reportingRuleExecutionResult.result;
    if (!fullResults) {
      return [];
    }

    return gatherSignalsFromResult(fullResults);
  },
};

const RuleInsights: GQLRuleInsightsResolvers = {
  async passRateData(rule, args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    return context.dataSources.ruleAPI.ruleInsights.getRulePassRateData(
      rule.id,
      rule.orgId,
      args.lookbackStartDate ? new Date(args.lookbackStartDate) : undefined,
    );
  },
  async latestVersionSamples(rule, _args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const samples =
      await context.dataSources.ruleAPI.ruleInsights.getRulePassingContentSamples(
        {
          ruleId: rule.id,
          orgId: rule.orgId,
          numSamples: 300,
          source: 'latestVersion',
        },
      );

    // TODO: type this properly. graphql queries would throw an exception if
    // asking for fields that aren't populated here. To provide them, we'll
    // have to update (and add better typings for) getRulePassingContentSamples.
    /* eslint-disable @typescript-eslint/no-explicit-any -- see TODO above */
    return samples.map((it) => ({
      ...it,
      itemId: it.contentId,
      itemData: jsonStringify(it.content),
      policyIds: it.policyIds,
      passed: true,
      ruleId: rule.id,
      ruleName: rule.name,
    })) as any[];
    /* eslint-enable @typescript-eslint/no-explicit-any */
  },
  async priorVersionSamples(rule, _args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const samples =
      await context.dataSources.ruleAPI.ruleInsights.getRulePassingContentSamples(
        {
          ruleId: rule.id,
          orgId: rule.orgId,
          numSamples: 300,
          source: 'priorVersion',
        },
      );

    // TODO: type this properly. See latestVersionSamples for context.
    /* eslint-disable @typescript-eslint/no-explicit-any -- see TODO above */
    return samples.map((it) => ({
      ...it,
      itemId: it.contentId,
      itemData: jsonStringify(it.content),
      policyIds: it.policyIds,
      passed: true,
      ruleId: rule.id,
      ruleName: rule.name,
    })) as any[];
    /* eslint-enable @typescript-eslint/no-explicit-any */
  },
};

const ReportingRuleInsights: GQLReportingRuleInsightsResolvers = {
  async passRateData(rule, args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    return context.dataSources.ruleAPI.ruleInsights.getRulePassRateData(
      rule.id,
      rule.orgId,
      args.lookbackStartDate ? new Date(args.lookbackStartDate) : undefined,
    );
  },
  async latestVersionSamples(rule, _args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const samples =
      await context.dataSources.ruleAPI.ruleInsights.getRulePassingContentSamples(
        {
          ruleId: rule.id,
          orgId: rule.orgId,
          source: 'latestVersion',
          numSamples: 300,
        },
      );

    // TODO: type this properly. graphql queries would throw an exception if
    // asking for fields that aren't populated here. To provide them, we'll
    // have to update (and add better typings for) getRulePassingContentSamples.
    /* eslint-disable @typescript-eslint/no-explicit-any -- see TODO above */
    return samples.map((it) => ({
      ...it,
      itemId: it.contentId,
      itemData: jsonStringify(it.content),
      policyIds: it.policyIds,
      passed: true,
      ruleId: rule.id,
      ruleName: rule.name,
    })) as any[];
    /* eslint-enable @typescript-eslint/no-explicit-any */
  },
  async priorVersionSamples(rule, _args, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const samples =
      await context.dataSources.ruleAPI.ruleInsights.getRulePassingContentSamples(
        {
          ruleId: rule.id,
          orgId: rule.orgId,
          source: 'priorVersion',
          numSamples: 300,
        },
      );

    // TODO: type this properly. See latestVersionSamples for context.
    /* eslint-disable @typescript-eslint/no-explicit-any -- see TODO above */
    return samples.map((it) => ({
      ...it,
      itemId: it.contentId,
      itemData: jsonStringify(it.content),
      policyIds: it.policyIds,
      passed: true,
      ruleId: rule.id,
      ruleName: rule.name,
    })) as any[];
    /* eslint-enable @typescript-eslint/no-explicit-any */
  },
};

const Query: GQLQueryResolvers = {
  async getUserStrikeCountDistribution(_, __, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }

    const allUserStrikeCounts =
      await context.services.UserStrikeService.getAllUserStrikeCountsForOrg(
        user.orgId,
      );
    const bucketedStrikeCounts = lodash.countBy(
      allUserStrikeCounts.filter((it) => it.strike_count > 0),
      (it) => it.strike_count,
    );
    return Object.entries(bucketedStrikeCounts).map((bucket) => ({
      numStrikes: Number(bucket[0]),
      numUsers: bucket[1],
    }));
  },
  async getFullRuleResultForItem(_, { input }, context) {
    const user = context.getUser();
    if (user == null) {
      throw unauthenticatedError('Authenticated user required');
    }
    const { ruleId, item, date, lookback } = input;
    try {
      const [samples, rule] = await Promise.all([
        context.dataSources.ruleAPI.ruleInsights.getRuleContentSamples({
          ruleId,
          orgId: user.orgId,
          source: lookback === 'LATEST' ? 'latestVersion' : 'priorVersion',
          itemIds: [item.id],
          numSamples: 1,
          dateFilter: date ?? undefined,
        }),
        context.dataSources.ruleAPI.getGraphQLRuleFromId(ruleId, user.orgId),
      ]);

      if (samples.length === 0) {
        throw makeNotFoundError('Item not found', { shouldErrorSpan: true });
      }

      const result = samples[0];

      // TODO: type this properly. graphql queries would throw an exception if
      // asking for fields that aren't populated here. To provide them, we'll
      // have to update (and add better typings for) getRulePassingContentSamples.
      /* eslint-disable @typescript-eslint/no-explicit-any -- see TODO above */
      return gqlSuccessResult(
        {
          ...result,
          itemId: result.contentId,
          itemData: jsonStringify(result.content),
          policyIds: result.policyIds,
          passed: true,
          ruleId: rule.id,
          ruleName: rule.name,
        } as any,
        'RuleExecutionResult',
      );
      /* eslint-enable @typescript-eslint/no-explicit-any */
    } catch (e) {
      if (isCoopErrorOfType(e, 'NotFoundError')) {
        return gqlErrorResult(e);
      }

      throw e;
    }
  },
};

const resolvers = {
  Query,
  ReportingRuleExecutionResult,
  ReportingRuleInsights,
  RuleInsights,
  RuleExecutionResult,
};

export { typeDefs, resolvers };
