import type { IMemberSessionRepository } from "@/internal/member-session/member-session.interfaces";
import {
  getViewPointAccountByContactId,
  getViewPointAccountById,
} from "@/internal/viewpoint/member";
import { env } from "@/lib/env";
import { logError } from "@/lib/logger";
import { hashPassword } from "@/lib/password";
import { getMemberPoints } from "@/viewpoint";
import type { AccountContact, AccountOwner } from "@/viewpoint/type";
import type { TMemberUserRepository } from "./member-user.repository";
import type { TMemberRepository } from "./member.repository";

interface TMemberServicesDeps {
  MemberRepository: TMemberRepository;
  MemberUserRepository: TMemberUserRepository;
  MemberSessionRepository: IMemberSessionRepository;
}

export function MemberServices({
  MemberRepository,
  MemberUserRepository,
  MemberSessionRepository,
}: TMemberServicesDeps) {
  const GetPaginatedMemberShips = async (query: {
    page: number;
    limit: number;
    searchTerm?: string;
    accountTypeIds?: string[];
    accountStatusIds?: string[];
    isActive?: boolean;
    createdFrom?: string;
    createdTo?: string;
    scopeTypeIds?: string[];
    scopeStatusIds?: string[];
  }) => {
    try {
      const records = await MemberRepository.FindPaginatedMemberships(query);
      return records;
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const GetAccountByMembershipNumber = async (membershipNumber: string) => {
    try {
      const vpAccount = await getViewPointAccountById(membershipNumber);
      if (!vpAccount) {
        return null;
      }
      return {
        AccountID: vpAccount.AccountID,
        AccountName: vpAccount.AccountName,
        AccountType: vpAccount.AccountType,
        AccountTypeID: vpAccount.AccountTypeID,
        ServiceCenter: vpAccount.ServiceCenter,
        ServiceCenterID: vpAccount.ServiceCenterID,
        AccountStatus: vpAccount.AccountStatus,
        AccountStatusID: vpAccount.AccountStatusID,
        DefaultChargeCenter: vpAccount.DefaultChargeCenter,
        JoinedDate: vpAccount.JoinedDate,
        BillingAddressLine1: vpAccount.BillingAddressLine1,
        BillingAddressLine2: vpAccount.BillingAddressLine2,
        BillingAddressState: vpAccount.BillingAddressState,
        BillingAddressCity: vpAccount.BillingAddressCity,
        BillingAddrCountry: vpAccount.BillingAddrCountry,
        BillingPostcode: vpAccount.BillingPostcode,
        Notes: vpAccount.Notes,
        CRMData: vpAccount.CRMData ?? [],
      };
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const GetMembersByMembershipNumber = async (membershipNumber: string) => {
    try {
      const memberRecords =
        await MemberRepository.FindMembersByMembershipNumber(membershipNumber);
      const vpAccount = await getViewPointAccountById(membershipNumber);

      const mapContact = (contact: AccountOwner | AccountContact) => {
        const coreMember = memberRecords.find(
          (item) => item.member_number === contact.ContactID.toString(),
        );
        return {
          member_id: coreMember?.member_id ?? null,
          member_number: contact.ContactID,
          membership_number: contact.AccountID,
          is_active: contact.Active,
          is_deleted: contact.Deleted,
          created_at: coreMember?.created_at ?? null,
          updated_at: coreMember?.updated_at ?? null,
          user_id: coreMember?.user_id ?? null,
          email: contact.Email,
          first_name: contact.FirstName,
          last_name: contact.LastName,
          is_family: contact.IsFamily,
          is_owner: contact.IsOwner,
          // Viewpoint's primary-contact flag. The member DB has no equivalent,
          // so this is the only place ownership priority can be read from.
          is_primary: contact.IsPrimary,
          user_is_active: coreMember?.user_is_active ?? null,
          user_is_deleted: coreMember?.user_is_deleted ?? null,
          account_type_id: coreMember?.account_type_id ?? null,
          account_type_name: vpAccount?.AccountType ?? null,
          account_type_ext_id: vpAccount?.AccountTypeID ?? null,
          account_type_is_active: coreMember?.account_type_is_active ?? null,
          account_status_id: coreMember?.account_status_id ?? null,
          account_status_name: vpAccount?.AccountStatus ?? null,
          account_status_ext_id: vpAccount?.AccountStatusID ?? null,
          account_status_is_active:
            coreMember?.account_status_is_active ?? null,
          address_line_one: contact.AddressLine1,
          address_line_two: contact.AddressLine2,
          city: contact.AddressCity,
          country: contact.AddrCountry,
          postcode: contact.Postcode,
          mobile: contact.Mobile ?? contact.Phone,
          nationality: contact.Nationality,
          state: contact.AddressState,
          date_of_birth: contact.DateOfBirth,
          avatar: coreMember?.avatar ?? null,
        };
      };

      /*
       * Primary first, then owners, then family, then the rest.
       *
       * Viewpoint returns Owners and Contacts as separate lists, and neither is
       * ordered by standing — so a child could lead the table while the primary
       * contact sat below the fold.
       */
      const rank = (contact: {
        is_primary?: boolean;
        is_owner?: boolean;
        is_family?: boolean;
      }) =>
        contact.is_primary
          ? 0
          : contact.is_owner
            ? 1
            : contact.is_family
              ? 2
              : 3;

      return [
        ...(vpAccount?.Owners ?? []).map(mapContact),
        ...(vpAccount?.Contacts ?? []).map(mapContact),
      ].sort(
        (a, b) =>
          rank(a) - rank(b) ||
          Number(a.member_number ?? 0) - Number(b.member_number ?? 0),
      );
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const GetMemberByMemberNumber = async (memberNumber: string) => {
    try {
      const memberRecords =
        await MemberRepository.FindMembersByMemberNumber(memberNumber);
      if (memberRecords.length === 0) {
        return null;
      }
      const vpContact = await getViewPointAccountByContactId(
        Number(memberRecords[0].member_number),
      );
      return {
        ...memberRecords[0],
        crmData: vpContact?.CRMData ?? [],
      };
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  // Sessions for a member record. Resolves the member's user_id here so the
  // caller only needs the member id; both reads hit the member DB.
  const GetMemberSessions = async (memberRecordID: string) => {
    try {
      const memberRecord =
        await MemberRepository.FindMemberRecordByID(memberRecordID);
      if (!memberRecord?.user_id) {
        return null;
      }
      const sessions = await MemberSessionRepository.FindMemberSessionsByUserId(
        memberRecord.user_id,
      );
      return sessions ?? [];
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  /**
   * Accounts dashboard aggregates.
   *
   * One query, folded here — see FindMembershipAnalytics for why.
   */
  const GetMembershipAnalytics = async (
    filters: {
      searchTerm?: string;
      accountTypeIds?: string[];
      accountStatusIds?: string[];
      isActive?: boolean;
      createdFrom?: string;
      createdTo?: string;
      scopeTypeIds?: string[];
      scopeStatusIds?: string[];
    } = {},
    countryLimit = 200,
  ) => {
    try {
      // The same filters and access slice the table uses, so the charts always
      // describe the rows underneath them.
      const rows = await MemberRepository.FindMembershipAnalytics(
        filters,
        countryLimit,
      );
      // Sequential: two scans of a busy DB, and running them together is what
      // used to time this endpoint out.
      const bookings = await MemberRepository.CountBookingTotals(filters);

      const totalsRow = rows.find((row) => row.kind === "total");
      const totals = {
        accounts: Number(totalsRow?.accounts ?? 0),
        members: Number(totalsRow?.count ?? 0),
        active: Number(totalsRow?.active ?? 0),
        inactive: Number(totalsRow?.inactive ?? 0),
      };

      const byAccountType = rows
        .filter((row) => row.kind === "type")
        .map((row) => ({
          id: String(row.id),
          name: row.name ?? "Unknown",
          count: Number(row.count),
        }))
        .sort((a, b) => b.count - a.count);

      const byAccountStatus = rows
        .filter((row) => row.kind === "status")
        .map((row) => ({
          id: String(row.id),
          name: row.name ?? "Unknown",
          count: Number(row.count),
        }))
        .sort((a, b) => b.count - a.count);

      const byCountry = rows
        .filter((row) => row.kind === "country")
        .map((row) => ({
          country: row.country ?? "Unknown",
          accountTypeId: String(row.id),
          accountTypeName: row.name ?? "Unknown",
          count: Number(row.count),
        }));

      return { totals, bookings, byAccountType, byAccountStatus, byCountry };
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  /**
   * Whether a scoped console user may open this record.
   *
   * Empty allow-lists mean unrestricted, so this only says no when a list
   * exists and the record falls outside it.
   */
  const IsMembershipInScope = async (
    params: { membershipNumber?: string; memberNumber?: string },
    scope: { accountTypeIds: string[]; accountStatusIds: string[] },
  ) => {
    if (
      scope.accountTypeIds.length === 0 &&
      scope.accountStatusIds.length === 0
    ) {
      return true;
    }
    const keys = await MemberRepository.FindMembershipScopeKeys(params);
    if (!keys) {
      // Unknown record: let the handler report "not found" rather than
      // masking it as a permission problem.
      return true;
    }
    if (
      scope.accountTypeIds.length > 0 &&
      !scope.accountTypeIds.includes(String(keys.account_type_id))
    ) {
      return false;
    }
    if (
      scope.accountStatusIds.length > 0 &&
      !scope.accountStatusIds.includes(String(keys.account_status_id))
    ) {
      return false;
    }
    return true;
  };

  const GetAccountOverview = async (membershipNumber: string) =>
    await MemberRepository.FindAccountOverview(membershipNumber);

  /**
   * Viewpoint points entitlement for an account, fetched on demand.
   *
   * Its own call so the member page loads without it and the console asks for
   * points only when someone wants to see them.
   */
  const GetMemberPointsBalance = async (membershipNumber: string) => {
    const vpAccount = await getViewPointAccountById(membershipNumber);
    // Same rule the points-transfer service uses: India accounts sit on a
    // different club/resort id.
    const isIndia = [vpAccount?.ServiceCenter, vpAccount?.AccountName]
      .filter(Boolean)
      .some((value) => String(value).toUpperCase().includes("INDIA"));
    const clubResortID = isIndia
      ? env.GetInt("INDIA_CLUB_RESORT_ID") || 3208
      : env.GetInt("NON_INDIA_CLUB_RESORT_ID") || 3206;

    const entitlements = await getMemberPoints(membershipNumber, clubResortID);
    const rows = Array.isArray(entitlements) ? entitlements : [];
    const balance = rows.reduce((total: number, row: any) => {
      const value = Number(
        row?.PointsBalance ?? row?.Balance ?? row?.Points ?? 0,
      );
      return total + (Number.isFinite(value) ? value : 0);
    }, 0);
    return { balance, clubResortID, entitlements: rows };
  };

  const GetMembershipFilterOptions = async () => {
    try {
      const [accountTypes, accountStatuses] = await Promise.all([
        MemberRepository.FindAccountTypes(),
        MemberRepository.FindAccountStatuses(),
      ]);
      return { accountTypes, accountStatuses };
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const GetMemberSavedSearches = async (memberRecordID: string) => {
    try {
      const records =
        await MemberRepository.FindSavedSearchesByMemberID(memberRecordID);
      return records;
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  const UpdateMemberPassword = async (
    memberRecordID: string,
    newPassword: string,
  ) => {
    try {
      const memberRecord =
        await MemberRepository.FindMemberRecordByID(memberRecordID);
      if (!memberRecord?.user_id) {
        return false;
      }
      const user = await MemberUserRepository.FindUserByID(
        memberRecord.user_id,
      );
      if (!user) {
        return false;
      }
      await MemberUserRepository.UpdateUserPassword({
        email: user.email,
        hashed_password: await hashPassword(newPassword),
      });
      return true;
    } catch (err) {
      logError(err);
      throw err;
    }
  };

  return {
    GetPaginatedMemberShips,
    GetAccountByMembershipNumber,
    GetMembersByMembershipNumber,
    GetMemberByMemberNumber,
    GetAccountOverview,
    IsMembershipInScope,
    GetMemberPointsBalance,
    GetMembershipAnalytics,
    GetMembershipFilterOptions,
    GetMemberSessions,
    GetMemberSavedSearches,
    UpdateMemberPassword,
  };
}

export type TMemberServices = ReturnType<typeof MemberServices>;
