import { prisma } from "../constants";
import { CompanyController } from "../controllers/CompanyController";
import { OpportunityController } from "../controllers/OpportunityController";
import UserController from "../controllers/UserController";
import GenericError from "../Errors/GenericError";
import { opportunityCw } from "../modules/cw-utils/opportunities/opportunities";
import { CWOpportunityCreate } from "../modules/cw-utils/opportunities/opportunity.types";

// ---------------------------------------------------------------------------
// Data-source helpers (DB-only, no cache)
// ---------------------------------------------------------------------------

/**
 * Build a CompanyController from a Prisma Company record.
 * DB-only - no cache or CW API calls.
 */
async function buildCompanyController(
  companyId: number
): Promise<CompanyController | undefined> {
  const company = await prisma.company.findFirst({
    where: { id: companyId },
    include: {
      contacts: true,
      companyAddresses: true,
    },
  });

  if (!company) return undefined;

  return new CompanyController(company);
}

export const opportunities = {
  /**
   * Create Opportunity
   *
   * Creates a new opportunity in ConnectWise, then stores the resulting
   * record in the local database and returns an OpportunityController.
   *
   * @param data — Fields required by the ConnectWise `POST /sales/opportunities` endpoint
   * @returns {Promise<OpportunityController>}
   */
  async createItem(data: CWOpportunityCreate): Promise<OpportunityController> {
    const cwData = await opportunityCw.create(data);
    const mapped = OpportunityController.mapCwToDb(cwData);

    // Resolve optional local FKs — nullify any that don't exist locally yet
    // (the sync may be behind; these are all nullable in the schema)
    const [
      companyExists,
      contactExists,
      siteExists,
      typeExists,
      stageExists,
      statusExists,
      locationExists,
      departmentExists,
      primaryRepExists,
      secondaryRepExists,
    ] = await Promise.all([
      cwData.company?.id
        ? prisma.company.findFirst({ where: { id: cwData.company.id }, select: { id: true } })
        : null,
      mapped.contactId != null
        ? prisma.contact.findFirst({ where: { id: mapped.contactId }, select: { id: true } })
        : null,
      mapped.siteId != null
        ? prisma.companyAddress.findFirst({ where: { id: mapped.siteId }, select: { id: true } })
        : null,
      mapped.typeId != null
        ? prisma.opportunityType.findFirst({ where: { id: mapped.typeId }, select: { id: true } })
        : null,
      mapped.stageId != null
        ? prisma.opportunityStage.findFirst({ where: { id: mapped.stageId }, select: { id: true } })
        : null,
      mapped.statusId != null
        ? prisma.opportunityStatus.findFirst({ where: { id: mapped.statusId }, select: { id: true } })
        : null,
      mapped.locationId != null
        ? prisma.corporateLocation.findFirst({ where: { id: mapped.locationId }, select: { id: true } })
        : null,
      mapped.departmentId != null
        ? prisma.internalDepartment.findFirst({ where: { id: mapped.departmentId }, select: { id: true } })
        : null,
      mapped.primarySalesRepId != null
        ? prisma.user.findFirst({ where: { cwIdentifier: mapped.primarySalesRepId }, select: { cwIdentifier: true } })
        : null,
      mapped.secondarySalesRepId != null
        ? prisma.user.findFirst({ where: { cwIdentifier: mapped.secondarySalesRepId }, select: { cwIdentifier: true } })
        : null,
    ]);

    const companyId = companyExists?.id ?? null;
    const contactId = contactExists?.id ?? null;
    const siteId = siteExists?.id ?? null;
    const typeId = typeExists?.id ?? null;
    const stageId = stageExists?.id ?? null;
    const statusId = statusExists?.id ?? null;
    const locationId = locationExists?.id ?? null;
    const departmentId = departmentExists?.id ?? null;
    const primarySalesRepId = primaryRepExists?.cwIdentifier ?? null;
    const secondarySalesRepId = secondaryRepExists?.cwIdentifier ?? null;

    // Strip fields returned by mapCwToDb that are not columns in the Prisma schema
    // (ratingName, ratingCwId, campaignName, primarySalesRepName, primarySalesRepIdentifier,
    //  secondarySalesRepName, secondarySalesRepIdentifier, cwLastUpdated).
    // Prisma will throw a validation error if unknown fields are passed to create().
    const {
      ratingName: _ratingName,
      ratingCwId: _ratingCwId,
      campaignName: _campaignName,
      primarySalesRepName: _primarySalesRepName,
      primarySalesRepIdentifier: _primarySalesRepIdentifier,
      secondarySalesRepName: _secondarySalesRepName,
      secondarySalesRepIdentifier: _secondarySalesRepIdentifier,
      cwLastUpdated: _cwLastUpdated,
      ...dbFields
    } = mapped;

    const record = await prisma.opportunity.create({
      data: {
        id: cwData.id,
        ...dbFields,
        typeId,
        stageId,
        statusId,
        locationId,
        departmentId,
        companyId,
        contactId,
        siteId,
        primarySalesRepId,
        secondarySalesRepId,
      },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        site: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
    });

    return new OpportunityController(record, {
      company: record.company
        ? new CompanyController(record.company)
        : undefined,
      primarySalesRep: record.primarySalesRep
        ? new UserController(record.primarySalesRep)
        : undefined,
      secondarySalesRep: record.secondarySalesRep
        ? new UserController(record.secondarySalesRep)
        : undefined,
    });
  },

  /**
   * Fetch Record (lightweight)
   *
   * Returns an OpportunityController backed only by the **database record**.
   * No ConnectWise API calls, no Redis lookups.
   *
   * @param identifier - The CW opportunity ID (number) or internal uid (string)
   * @returns {Promise<OpportunityController>}
   */
  async fetchRecord(
    identifier: string | number
  ): Promise<OpportunityController> {
    const isNumeric =
      typeof identifier === "number" || /^\d+$/.test(String(identifier));

    const record = await prisma.opportunity.findFirst({
      where: isNumeric
        ? { cwOpportunityId: Number(identifier) }
        : { uid: identifier as string },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        site: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
    });

    if (!record) {
      throw new GenericError({
        message: "Opportunity not found",
        name: "OpportunityNotFound",
        cause: `No opportunity exists with identifier '${identifier}'`,
        status: 404,
      });
    }

    return new OpportunityController(record, {
      company: record.company
        ? new CompanyController(record.company)
        : undefined,
      primarySalesRep: record.primarySalesRep
        ? new UserController(record.primarySalesRep)
        : undefined,
      secondarySalesRep: record.secondarySalesRep
        ? new UserController(record.secondarySalesRep)
        : undefined,
    });
  },

  /**
   * Fetch Opportunity
   *
   * Fetch an opportunity by its internal uid or CW opportunity ID
   * and return an OpportunityController instance.
   *
   * Data is loaded from the local database only.
   *
   * @param identifier - The internal uid (string) or CW opportunity ID (number)
   * @param opts - Optional flags
   * @param opts.fresh - Ignored (kept for API compatibility)
   * @returns {Promise<OpportunityController>}
   */
  async fetchItem(
    identifier: string | number,
    opts?: { fresh?: boolean }
  ): Promise<OpportunityController> {
    const isNumeric =
      typeof identifier === "number" || /^\d+$/.test(String(identifier));

    const record = await prisma.opportunity.findFirst({
      where: isNumeric
        ? { id: Number(identifier) }
        : { uid: identifier as string },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        site: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
    });

    if (!record) {
      throw new GenericError({
        message: "Opportunity not found",
        name: "OpportunityNotFound",
        cause: `No opportunity exists with identifier '${identifier}'`,
        status: 404,
      });
    }

    return new OpportunityController(record, {
      company: record.company
        ? new CompanyController(record.company)
        : undefined,
      primarySalesRep: record.primarySalesRep
        ? new UserController(record.primarySalesRep)
        : undefined,
      secondarySalesRep: record.secondarySalesRep
        ? new UserController(record.secondarySalesRep)
        : undefined,
    });
  },

  /**
   * Fetch All Opportunities (Paginated)
   *
   * @param page - Page number (1-based)
   * @param rpp  - Records per page
   * @param opts - Optional filters
   * @returns {Promise<OpportunityController[]>}
   */
  async fetchPages(
    page: number,
    rpp: number,
    opts?: { includeClosed?: boolean }
  ): Promise<OpportunityController[]> {
    const skip = (Math.max(page, 1) - 1) * rpp;

    const items = await prisma.opportunity.findMany({
      where: opts?.includeClosed ? undefined : { closedFlag: false },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
      skip,
      take: rpp,
      orderBy: { id: "desc" },
    });

    return items.map(
      (item) =>
        new OpportunityController(item, {
          company: item.company
            ? new CompanyController(item.company)
            : undefined,
          primarySalesRep: item.primarySalesRep
            ? new UserController(item.primarySalesRep)
            : undefined,
          secondarySalesRep: item.secondarySalesRep
            ? new UserController(item.secondarySalesRep)
            : undefined,
        })
    );
  },

  /**
   * Search Opportunities
   *
   * Search opportunities by name with pagination support.
   *
   * @param query - Search query string
   * @param page  - Page number (1-based)
   * @param rpp   - Records per page
   * @param opts  - Optional filters
   * @returns {Promise<OpportunityController[]>}
   */
  async search(
    query: string,
    page: number,
    rpp: number,
    opts?: { includeClosed?: boolean }
  ): Promise<OpportunityController[]> {
    const skip = (Math.max(page, 1) - 1) * rpp;
    const numericQuery = /^\d+$/.test(query.trim())
      ? Number(query.trim())
      : null;

    const items = await prisma.opportunity.findMany({
      where: {
        ...(opts?.includeClosed ? {} : { closedFlag: false }),
        OR: [
          { name: { contains: query, mode: "insensitive" } },
          { notes: { contains: query, mode: "insensitive" } },
          ...(numericQuery !== null ? [{ id: { equals: numericQuery } }] : []),
        ],
      },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
      skip,
      take: rpp,
      orderBy: { createdAt: "desc" },
    });

    return items.map(
      (item) =>
        new OpportunityController(item, {
          company: item.company
            ? new CompanyController(item.company)
            : undefined,
          primarySalesRep: item.primarySalesRep
            ? new UserController(item.primarySalesRep)
            : undefined,
          secondarySalesRep: item.secondarySalesRep
            ? new UserController(item.secondarySalesRep)
            : undefined,
        })
    );
  },

  /**
   * Count Opportunities
   *
   * @param opts - Optional filters
   * @returns {Promise<number>}
   */
  async count(opts?: { openOnly?: boolean }): Promise<number> {
    return prisma.opportunity.count({
      where: opts?.openOnly ? { closedFlag: false } : undefined,
    });
  },

  /**
   * Count Search Results
   *
   * @param query - Search query string
   * @param opts  - Optional filters
   * @returns {Promise<number>}
   */
  async searchCount(
    query: string,
    opts?: { includeClosed?: boolean }
  ): Promise<number> {
    const numericQuery = /^\d+$/.test(query.trim())
      ? Number(query.trim())
      : null;

    return prisma.opportunity.count({
      where: {
        ...(opts?.includeClosed ? {} : { closedFlag: false }),
        OR: [
          { name: { contains: query, mode: "insensitive" } },
          { notes: { contains: query, mode: "insensitive" } },
          ...(numericQuery !== null ? [{ id: { equals: numericQuery } }] : []),
        ],
      },
    });
  },

  /**
   * Fetch Opportunities by Company
   *
   * Fetch all opportunities for a company by its internal company ID.
   *
   * @param companyId - The internal company ID
   * @param opts - Optional filters
   * @returns {Promise<OpportunityController[]>}
   */
  async fetchByCompany(
    companyId: number,
    opts?: { includeClosed?: boolean }
  ): Promise<OpportunityController[]> {
    const items = await prisma.opportunity.findMany({
      where: {
        companyId,
        ...(opts?.includeClosed ? {} : { closedFlag: false }),
      },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
      orderBy: { expectedCloseDate: "asc" },
    });

    return items.map(
      (item) =>
        new OpportunityController(item, {
          company: item.company
            ? new CompanyController(item.company)
            : undefined,
          primarySalesRep: item.primarySalesRep
            ? new UserController(item.primarySalesRep)
            : undefined,
          secondarySalesRep: item.secondarySalesRep
            ? new UserController(item.secondarySalesRep)
            : undefined,
        })
    );
  },

  /**
   * Fetch Opportunities by User
   *
   * Returns all opportunities where the given user (by internal User ID) is
   * assigned as the primary or secondary sales rep.
   *
   * @param userId - Internal User `id` (cuid)
   * @param opts   - Optional filters
   * @returns {Promise<OpportunityController[]>}
   */
  async fetchByUser(
    userId: string,
    opts?: { includeClosed?: boolean }
  ): Promise<OpportunityController[]> {
    const user = await prisma.user.findFirst({
      where: { id: userId },
      select: { cwIdentifier: true },
    });

    if (!user) {
      throw new GenericError({
        message: "User not found",
        name: "UserNotFound",
        cause: `No user exists with id '${userId}'`,
        status: 404,
      });
    }

    if (!user.cwIdentifier) {
      return [];
    }

    const items = await prisma.opportunity.findMany({
      where: {
        OR: [
          { primarySalesRepId: user.cwIdentifier },
          { secondarySalesRepId: user.cwIdentifier },
        ],
        ...(opts?.includeClosed
          ? {}
          : {
              closedFlag: false,
              NOT: {
                status: {
                  OR: [
                    { wonFlag: true },
                    { lostFlag: true },
                    { closeFlag: true },
                  ],
                },
              },
            }),
      },
      include: {
        company: { include: { contacts: true, companyAddresses: true } },
        contact: true,
        type: true,
        status: true,
        primarySalesRep: { include: { roles: true } },
        secondarySalesRep: { include: { roles: true } },
        stage: true,
        taxCode: true,
      },
      orderBy: { expectedCloseDate: "asc" },
    });

    return items.map(
      (item) =>
        new OpportunityController(item, {
          company: item.company
            ? new CompanyController(item.company)
            : undefined,
          primarySalesRep: item.primarySalesRep
            ? new UserController(item.primarySalesRep)
            : undefined,
          secondarySalesRep: item.secondarySalesRep
            ? new UserController(item.secondarySalesRep)
            : undefined,
        })
    );
  },

  /**
   * Delete Opportunity
   *
   * Deletes an opportunity from ConnectWise and removes the corresponding
   * record (along with its associated ProductData) from the local database.
   *
   * @param identifier - The internal uid (string) or CW opportunity ID (number)
   */
  async deleteItem(identifier: string | number): Promise<void> {
    const isNumeric =
      typeof identifier === "number" || /^\d+$/.test(String(identifier));

    const record = await prisma.opportunity.findFirst({
      where: isNumeric
        ? { id: Number(identifier) }
        : { uid: identifier as string },
      select: { uid: true, id: true },
    });

    if (!record) {
      throw new GenericError({
        message: "Opportunity not found",
        name: "OpportunityNotFound",
        cause: `No opportunity exists with identifier '${identifier}'`,
        status: 404,
      });
    }

    await opportunityCw.delete(record.id);

    await prisma.$transaction([
      prisma.productData.deleteMany({ where: { opportunityId: record.id } }),
      prisma.opportunity.delete({ where: { uid: record.uid } }),
    ]);
  },
};
