import { GraphQLError } from 'graphql';
import { Currency } from '../../../shared/enums.js';
import { formatFinancialAmount } from '../../../shared/helpers/amount.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ChargesProvider } from '../../charges/providers/charges.provider.js';
import { BusinessesProvider } from '../../financial-entities/providers/businesses.provider.js';
import {
  tradeTypeToRaw,
  transactionTypeToRaw,
} from '../helpers/security-execution-enums.helper.js';
import { calculateSecurityPosition, isOpenPosition } from '../helpers/security-position.helper.js';
import { ForeignSecuritiesProvider } from '../providers/foreign-securities.provider.js';
import { SecurityBusinessesProvider } from '../providers/security-businesses.provider.js';
import type { ForeignSecuritiesModule } from '../types.js';

/**
 * Falls back to the schema default, which graphql-js applies for an omitted argument but not for
 * an explicit `null`.
 */
const DEFAULT_SECURITY_EXECUTIONS_LIMIT = 100;

/** An amount the executions imply, or null when they imply nothing. */
const positionAmount = (value: number | null, currency: string | null) =>
  value == null || currency == null ? null : formatFinancialAmount(value, currency);

export const securityBusinessesResolvers: ForeignSecuritiesModule.Resolvers = {
  Query: {
    allSecurityBusinesses: async (_, __, { injector }) => {
      const securityBusinesses = await injector
        .get(SecurityBusinessesProvider)
        .getAllSecurityBusinesses();

      const businesses = await injector
        .get(BusinessesProvider)
        .getBusinessByIdLoader.loadMany(securityBusinesses.map(security => security.id));

      // A security row whose business vanished is a broken FK, not a reason to fail the query.
      return businesses.filter(
        (business): business is Exclude<(typeof businesses)[number], Error | undefined | null> =>
          business != null && !(business instanceof Error),
      );
    },
    securityHoldings: async (_, { includeClosed }, { injector }) => {
      const securityBusinesses = await injector
        .get(SecurityBusinessesProvider)
        .getAllSecurityBusinesses();
      const executionsByBusinessId = await injector
        .get(ForeignSecuritiesProvider)
        .getExecutionsBySecurityBusiness();

      return (
        securityBusinesses
          .map(securityBusiness => ({
            id: securityBusiness.id,
            security: securityBusiness,
            position: {
              id: securityBusiness.id,
              ...calculateSecurityPosition(executionsByBusinessId.get(securityBusiness.id) ?? []),
            },
          }))
          // Closed positions are the bulk of a long-lived portfolio, so the screen asks for them
          // explicitly. A security with nothing ingested against it reads as closed too.
          .filter(holding => includeClosed || isOpenPosition(holding.position))
          // Ordered here so an unsorted render is stable across requests.
          .sort((a, b) =>
            (a.security.eng_name ?? a.security.isin).localeCompare(
              b.security.eng_name ?? b.security.isin,
            ),
          )
      );
    },
    securityBusinessHistory: async (_, { businessId }, { injector }) => {
      const securityBusiness = await injector
        .get(SecurityBusinessesProvider)
        .getSecurityBusinessByIdLoader.load(businessId);
      if (!securityBusiness) {
        throw new GraphQLError(`Business ID="${businessId}" is not a security`);
      }

      const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
      const { executions, transactionByExecutionId } = await injector
        .get(ForeignSecuritiesProvider)
        .getSecurityBusinessHistory(businessId, ownerId);

      return {
        id: businessId,
        security: securityBusiness,
        position: { id: businessId, ...calculateSecurityPosition(executions) },
        executions: executions.map(execution => ({
          id: execution.id,
          execution,
          securityBusinessId: businessId,
          transaction: transactionByExecutionId.get(execution.id) ?? null,
        })),
      };
    },
    securityExecutions: async (_, { filters, page, limit, includeCharges }, { injector }) => {
      const { ownerId } = await injector.get(AdminContextProvider).getVerifiedAdminContext();

      return injector.get(ForeignSecuritiesProvider).getSecurityExecutionsPage({
        ownerId,
        page: page ?? 0,
        limit: limit ?? DEFAULT_SECURITY_EXECUTIONS_LIMIT,
        includeCharges: includeCharges ?? false,
        filters: {
          securityBusinessIds: filters?.securityBusinessIds,
          isins: filters?.isins,
          symbols: filters?.symbols,
          fromTradeDate: filters?.fromTradeDate,
          toTradeDate: filters?.toTradeDate,
          // Translated here rather than in the provider so the bank's Hebrew stays confined to
          // the enum helper — the provider filters on labels and never learns what they say.
          rawTradeTypes: filters?.tradeTypes?.map(tradeType => tradeTypeToRaw[tradeType]),
          rawTransactionTypes: filters?.transactionTypes?.map(
            transactionType => transactionTypeToRaw[transactionType],
          ),
        },
      });
    },
  },
  PaginatedSecurityExecutions: {
    nodes: executionsPage => executionsPage.nodes,
    pageInfo: executionsPage => ({
      totalRecords: executionsPage.totalRecords,
      totalPages: Math.ceil(executionsPage.totalRecords / executionsPage.pageSize),
      currentPage: executionsPage.currentPage,
      pageSize: executionsPage.pageSize,
    }),
  },
  SecurityHistoryExecution: {
    id: historyExecution => historyExecution.id,
    execution: historyExecution => historyExecution.execution,
    securityBusiness: async (historyExecution, _, { injector }) => {
      const securityBusiness = await injector
        .get(SecurityBusinessesProvider)
        .getSecurityBusinessByIdLoader.load(historyExecution.securityBusinessId);
      if (!securityBusiness) {
        throw new GraphQLError(
          `Business ID="${historyExecution.securityBusinessId}" is not a security`,
        );
      }
      return securityBusiness;
    },
    // Transaction concrete types are mapped to their id (see codegen.ts mappers); Charge is
    // mapped to its row, so it has to be loaded.
    transaction: historyExecution => historyExecution.transaction?.id ?? null,
    charge: async (historyExecution, _, { injector }) => {
      const chargeId = historyExecution.transaction?.charge_id;
      if (!chargeId) {
        return null;
      }
      return (await injector.get(ChargesProvider).getChargeByIdLoader.load(chargeId)) ?? null;
    },
  },
  SecurityPosition: {
    id: position => position.id,
    quantity: position => position.quantity,
    // A security with no ingested executions has no currency to report an amount in, and
    // `formatFinancialAmount` would fall back to the local one — turning "nothing is known"
    // into a confident ILS 0. Null is the honest answer; the client renders it as an em dash.
    averageCost: position => positionAmount(position.averageCost, position.currency),
    totalBought: position => positionAmount(position.totalBought, position.currency),
    totalSold: position => positionAmount(position.totalSold, position.currency),
    historyStartDate: position => position.historyStartDate,
    lastExecutionDate: position => position.lastExecutionDate,
  },
  LtdFinancialEntity: {
    securityInfo: async (business, _, { injector }) =>
      (await injector
        .get(SecurityBusinessesProvider)
        .getSecurityBusinessByIdLoader.load(business.id)) ?? null,
  },
  SecurityBusiness: {
    id: securityBusiness => securityBusiness.id,
    ownerId: securityBusiness => securityBusiness.owner_id,
    business: async (securityBusiness, _, { injector }) => {
      const business = await injector
        .get(BusinessesProvider)
        .getBusinessByIdLoader.load(securityBusiness.id);
      if (!business) {
        throw new GraphQLError(`Business ID="${securityBusiness.id}" not found`);
      }
      return business;
    },
    isin: securityBusiness => securityBusiness.isin,
    identifiers: async (securityBusiness, _, { injector }) =>
      injector
        .get(SecurityBusinessesProvider)
        .getIdentifiersByBusinessIdLoader.load(securityBusiness.id),
    symbol: securityBusiness => securityBusiness.symbol,
    engName: securityBusiness => securityBusiness.eng_name,
    hebName: securityBusiness => securityBusiness.heb_name,
    exchange: securityBusiness => securityBusiness.exchange,
    // The column is accounter_schema.currency, whose values are the GraphQL enum's — the same
    // cast the financial-accounts resolvers use for their currency columns.
    currencyCode: securityBusiness => securityBusiness.currency_code as Currency | null,
    itemType: securityBusiness => securityBusiness.item_type,
    stockType: securityBusiness => securityBusiness.stock_type,
    isEtf: securityBusiness => securityBusiness.is_etf,
    isForeign: securityBusiness => securityBusiness.is_foreign,
    issuerCountryCode: securityBusiness => securityBusiness.issuer_country_code,
  },
  SecurityIdentifier: {
    id: identifier => identifier.id,
    type: identifier => identifier.identifier_type,
    value: identifier => identifier.identifier_value,
  },
};
