import {
  Company,
  CompanyAddress,
  Contact,
} from "../../generated/prisma/client";
import { connectWiseApi, prisma } from "../constants";
import GenericError from "../Errors/GenericError";
import { processConfigurationResponse } from "../modules/cw-utils/configurations/processConfigurationResponse";
import { withCwRetry } from "../modules/cw-utils/withCwRetry";
import type { ConfigurationResponse } from "../types/ConnectWiseTypes";

// Type for company data with relations
type CompanyWithRelations = Company & {
  identifier?: string | null;
  contacts?: Contact[];
  companyAddresses?: CompanyAddress[];
};

/** CW data blob optionally hydrated from the ConnectWise API. */
export interface CompanyCwData {
  company: {
    addressLine1?: string | null;
    addressLine2?: string | null;
    city?: string | null;
    state?: string | null;
    zip?: string | null;
    country?: { name: string } | null;
    [key: string]: any;
  };
  defaultContact?: {
    id: number;
    firstName: string;
    lastName: string;
    inactiveFlag?: boolean;
    title?: string | null;
    defaultPhoneNbr?: string | null;
    communicationItems?: Array<{ type: { name: string }; value: string }>;
    [key: string]: any;
  } | null;
  allContacts?: Array<{
    id: number;
    firstName: string;
    lastName: string;
    inactiveFlag?: boolean;
    title?: string | null;
    defaultPhoneNbr?: string | null;
    communicationItems?: Array<{ type: { name: string }; value: string }>;
    [key: string]: any;
  }>;
}

/**
 * Company Controller
 *
 * This class manages company data from the local database.
 * Data is synced from ConnectWise via the dalpuri service.
 */
export class CompanyController {
  /** Internal string UUID — the Prisma primary key (`Company.uid`). */
  public readonly id: string;
  /** Numeric ConnectWise company ID (`Company.id`). */
  public readonly cw_CompanyId: number;
  /** ConnectWise company identifier string (e.g. "TestCo"). */
  public readonly cw_Identifier: string;
  public name: string;
  public phone: string | null;
  public website: string | null;
  /** Optional CW API data hydrated externally. */
  public cw_Data: CompanyCwData | undefined;

  /** The raw numeric Prisma id — used internally for DB queries. */
  private readonly _numericId: number;
  private _contacts: Contact[] = [];
  private _addresses: CompanyAddress[] = [];
  private _defaultContact: Contact | null = null;
  private _defaultAddress: CompanyAddress | null = null;

  constructor(companyData: CompanyWithRelations, cwData?: CompanyCwData) {
    // `uid` is the internal string PK; `id` is the numeric CW company ID
    this.id = companyData.uid;
    this._numericId = companyData.id;
    this.cw_CompanyId = companyData.id;
    this.cw_Identifier = (companyData as any).identifier ?? "";
    this.name = companyData.name;
    this.phone = companyData.phone ?? null;
    this.website = companyData.website ?? null;
    this.cw_Data = cwData;

    if (companyData.contacts) {
      this._contacts = companyData.contacts;
      this._defaultContact =
        companyData.contacts.find((c) => c.default) ?? null;
    }

    if (companyData.companyAddresses) {
      this._addresses = companyData.companyAddresses;
      this._defaultAddress =
        companyData.companyAddresses.find((a) => a.defaultFlag) ?? null;
    }
  }

  /**
   * Hydrate Data
   *
   * Loads contacts and addresses from the local database if not already loaded.
   */
  public async hydrateData() {
    if (this._contacts.length === 0) {
      this._contacts = await prisma.contact.findMany({
        where: { companyId: this._numericId },
      });
      this._defaultContact = this._contacts.find((c) => c.default) ?? null;
    }

    if (this._addresses.length === 0) {
      this._addresses = await prisma.companyAddress.findMany({
        where: { companyId: this._numericId },
      });
      this._defaultAddress = this._addresses.find((a) => a.defaultFlag) ?? null;
    }

    return this;
  }

  /**
   * Refresh from DB
   *
   * Reloads the company data from the local database.
   */
  public async refreshFromDb() {
    const data = await prisma.company.findUnique({
      where: { id: this._numericId },
    });

    if (data) {
      this.name = data.name;
      this.phone = data.phone;
      this.website = data.website;
    }

    return this;
  }

  /**
   * Fetch Company Configurations
   *
   * Fetches configurations directly from ConnectWise.
   */
  public async fetchConfigurations() {
    const pageSize = 1000;
    const conditions = encodeURIComponent(`company/id=${this._numericId}`);
    const configurations: ConfigurationResponse = [];

    try {
      for (let page = 1; ; page++) {
        const response = await withCwRetry(
          () =>
            connectWiseApi.get<ConfigurationResponse>(
              `/company/configurations?page=${page}&pageSize=${pageSize}&conditions=${conditions}`,
            ),
          { label: `company-configurations:${this._numericId}:page-${page}` },
        );

        const items = Array.isArray(response.data) ? response.data : [];
        configurations.push(...items);

        if (items.length < pageSize) break;
      }

      return processConfigurationResponse(configurations);
    } catch (error: any) {
      const cwStatus = Number(error?.response?.status);
      const status = cwStatus >= 400 && cwStatus <= 599 ? cwStatus : 502;

      throw new GenericError({
        message: `Failed to fetch company configurations from ConnectWise`,
        name: "ConnectWiseFetchFailed",
        cause:
          error?.response?.data?.message ??
          error?.response?.statusText ??
          error?.message ??
          "Unknown ConnectWise error",
        status,
      });
    }
  }

  /**
   * Fetch Company Sites
   *
   * Retrieves all sites (addresses) for this company from local DB.
   */
  public async fetchSites() {
    const sites = await prisma.companyAddress.findMany({
      where: { companyId: this._numericId },
    });

    return sites.map((site) => ({
      id: site.id,
      name: site.name,
      addressLine1: site.addressLine1,
      addressLine2: site.addressLine2,
      city: site.city,
      state: site.state,
      zip: site.zipCode,
      country: site.country,
      phone: site.phone,
      fax: site.fax,
      defaultFlag: site.defaultFlag,
      inactiveFlag: site.inactiveFlag,
    }));
  }

  /**
   * Fetch Company Site by ID
   *
   * Retrieves a single site by its ID from local DB.
   */
  public async fetchSite(siteId: number) {
    const site = await prisma.companyAddress.findFirst({
      where: { id: siteId, companyId: this._numericId },
    });

    if (!site) return null;

    return {
      id: site.id,
      name: site.name,
      addressLine1: site.addressLine1,
      addressLine2: site.addressLine2,
      city: site.city,
      state: site.state,
      zip: site.zipCode,
      country: site.country,
      phone: site.phone,
      fax: site.fax,
      defaultFlag: site.defaultFlag,
      inactiveFlag: site.inactiveFlag,
    };
  }

  /**
   * Get all contacts
   */
  public getContacts() {
    return this._contacts;
  }

  /**
   * Get default contact
   */
  public getDefaultContact() {
    return this._defaultContact;
  }

  /**
   * Get default address
   */
  public getDefaultAddress() {
    return this._defaultAddress;
  }

  private _serializeContact(contact: {
    id: number;
    firstName: string;
    lastName: string;
    inactiveFlag?: boolean;
    title?: string | null;
    defaultPhoneNbr?: string | null;
    communicationItems?: Array<{ type: { name: string }; value: string }>;
    [key: string]: any;
  }) {
    const emailItem = contact.communicationItems?.find(
      (ci) => ci.type?.name === "Email"
    );
    return {
      cwId: contact.id,
      firstName: contact.firstName,
      lastName: contact.lastName,
      inactive: contact.inactiveFlag ?? false,
      title: contact.title ?? null,
      phone: contact.defaultPhoneNbr ?? null,
      email: emailItem?.value ?? null,
    };
  }

  public toJson(opts?: {
    includeAddress?: boolean;
    includePrimaryContact?: boolean;
    includeAllContacts?: boolean;
    includeAllAddresses?: boolean;
  }) {
    const cw_Data: Record<string, unknown> = {};

    if (opts?.includeAddress) {
      if (this.cw_Data) {
        const addr = this.cw_Data.company;
        cw_Data.address = {
          line1: addr.addressLine1 ?? null,
          line2: addr.addressLine2 ?? null,
          city: addr.city ?? null,
          state: addr.state ?? null,
          zip: addr.zip ?? null,
          country: addr.country?.name ?? "United States",
        };
      } else if (this._defaultAddress) {
        const addr = this._defaultAddress;
        cw_Data.address = {
          line1: addr.addressLine1 ?? null,
          line2: addr.addressLine2 ?? null,
          city: addr.city ?? null,
          state: addr.state ?? null,
          zip: addr.zipCode ?? null,
          country: addr.country ?? "United States",
        };
      }
    }

    if (opts?.includeAllAddresses) {
      cw_Data.allAddresses = this._addresses.map((addr) => ({
        id: addr.id,
        uid: addr.uid,
        name: addr.name,
        description: addr.description ?? null,
        defaultFlag: addr.defaultFlag,
        inactiveFlag: addr.inactiveFlag,
        addressLine1: addr.addressLine1 ?? null,
        addressLine2: addr.addressLine2 ?? null,
        city: addr.city ?? null,
        state: addr.state ?? null,
        zip: addr.zipCode ?? null,
        country: addr.country ?? null,
        phone: addr.phone ?? null,
      }));
    }

    if (opts?.includePrimaryContact) {
      if (this.cw_Data?.defaultContact) {
        cw_Data.primaryContact = this._serializeContact(
          this.cw_Data.defaultContact
        );
      } else if (this._defaultContact) {
        const c = this._defaultContact;
        cw_Data.primaryContact = {
          cwId: c.id,
          firstName: c.firstName,
          lastName: c.lastName,
          inactive: !c.active,
          title: c.title ?? null,
          phone: c.phone ?? null,
          email: c.email ?? null,
        };
      }
    }

    if (opts?.includeAllContacts) {
      if (this.cw_Data?.allContacts) {
        cw_Data.allContacts = this.cw_Data.allContacts.map((c) =>
          this._serializeContact(c)
        );
      } else if (this._contacts.length > 0) {
        cw_Data.allContacts = this._contacts.map((c) => ({
          cwId: c.id,
          firstName: c.firstName,
          lastName: c.lastName,
          inactive: !c.active,
          title: c.title ?? null,
          phone: c.phone ?? null,
          email: c.email ?? null,
        }));
      }
    }

    return {
      id: this.id,
      name: this.name,
      cw_Identifier: this.cw_Identifier,
      cw_CompanyId: this.cw_CompanyId,
      cw_Data,
    };
  }
}
