import {
  FinancialRow,
  InsertPatient,
  Patient,
  UpdatePatient,
} from "@repo/db/types";
import { prisma as db } from "@repo/db/client";

export interface IStorage {
  // Patient methods
  getPatient(id: number): Promise<Patient | undefined>;
  getPatientByInsuranceId(insuranceId: string): Promise<Patient | null>;
  getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise<Patient | null>;
  getAllPatients(): Promise<Patient[]>;
  getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
  getPatientsByIds(ids: number[]): Promise<Patient[]>;
  createPatient(patient: InsertPatient): Promise<Patient>;
  updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
  touchPatient(id: number): Promise<void>;
  deletePatient(id: number): Promise<void>;
  searchPatients(args: {
    filters: any;
    limit: number;
    offset: number;
  }): Promise<
    {
      id: number;
      firstName: string | null;
      lastName: string | null;
      phone: string | null;
      gender: string | null;
      dateOfBirth: Date;
      insuranceId: string | null;
      insuranceProvider: string | null;
      status: string;
    }[]
  >;
  getTotalPatientCount(): Promise<number>;
  countPatients(filters: any): Promise<number>; // optional but useful
  getPatientFinancialRows(
    patientId: number,
    limit?: number,
    offset?: number
  ): Promise<{ rows: any[]; totalCount: number }>;
}

export const patientsStorage: IStorage = {
  // Patient methods
  async getPatient(id: number): Promise<Patient | undefined> {
    const patient = await db.patient.findUnique({ where: { id } });
    return patient ?? undefined;
  },

  async getAllPatients(): Promise<Patient[]> {
    return await db.patient.findMany();
  },

  async getPatientByInsuranceId(insuranceId: string): Promise<Patient | null> {
    return db.patient.findFirst({
      where: { insuranceId },
    });
  },

  async getPatientByInsuranceIdAndDob(insuranceId: string, dob: Date): Promise<Patient | null> {
    const startOfDay = new Date(dob);
    startOfDay.setHours(0, 0, 0, 0);
    const endOfDay = new Date(dob);
    endOfDay.setHours(23, 59, 59, 999);
    return db.patient.findFirst({
      where: {
        insuranceId,
        dateOfBirth: { gte: startOfDay, lte: endOfDay },
      },
    });
  },

  async getRecentPatients(limit: number, offset: number): Promise<Patient[]> {
    return db.patient.findMany({
      skip: offset,
      take: limit,
      orderBy: { updatedAt: "desc" },
    });
  },

  async getPatientsByIds(ids: number[]): Promise<Patient[]> {
    if (!ids || ids.length === 0) return [];
    const uniqueIds = Array.from(new Set(ids));
    return db.patient.findMany({
      where: { id: { in: uniqueIds } },
      select: {
        id: true,
        firstName: true,
        lastName: true,
        phone: true,
        email: true,
        dateOfBirth: true,
        gender: true,
        insuranceId: true,
        insuranceProvider: true,
        status: true,
        userId: true,
        createdAt: true,
        updatedAt: true,
      },
    });
  },

  async createPatient(patient: InsertPatient): Promise<Patient> {
    return await db.patient.create({ data: patient as Patient });
  },

  async updatePatient(id: number, updateData: UpdatePatient): Promise<Patient> {
    try {
      return await db.patient.update({
        where: { id },
        data: updateData as Patient,
      });
    } catch (err) {
      throw new Error(`Patient with ID ${id} not found`);
    }
  },

  async touchPatient(id: number): Promise<void> {
    try {
      await db.patient.update({
        where: { id },
        data: { updatedAt: new Date() },
      });
    } catch (_) {}
  },

  async deletePatient(id: number): Promise<void> {
    try {
      await db.patient.delete({ where: { id } });
    } catch (err) {
      console.error("Error deleting patient:", err);
      throw new Error(`Failed to delete patient: ${err}`);
    }
  },

  async searchPatients({
    filters,
    limit,
    offset,
  }: {
    filters: any;
    limit: number;
    offset: number;
  }) {
    return db.patient.findMany({
      where: filters,
      orderBy: { updatedAt: "desc" },
      take: limit,
      skip: offset,
      select: {
        id: true,
        firstName: true,
        lastName: true,
        phone: true,
        gender: true,
        dateOfBirth: true,
        insuranceId: true,
        insuranceProvider: true,
        status: true,
      },
    });
  },

  async getTotalPatientCount(): Promise<number> {
    return db.patient.count();
  },

  async countPatients(filters: any) {
    return db.patient.count({ where: filters });
  },

  async getPatientFinancialRows(patientId: number, limit = 50, offset = 0) {
    return getPatientFinancialRowsFn(patientId, limit, offset);
  },
};

export const getPatientFinancialRowsFn = async (
  patientId: number,
  limit = 50,
  offset = 0
): Promise<{ rows: FinancialRow[]; totalCount: number }> => {
  try {
    // Count claims and orphan payments
    const [[{ count_claims }], [{ count_orphan_payments }]] =
      (await Promise.all([
        db.$queryRaw`SELECT COUNT(1) AS count_claims FROM "Claim" c WHERE c."patientId" = ${patientId}`,
        db.$queryRaw`SELECT COUNT(1) AS count_orphan_payments FROM "Payment" p WHERE p."patientId" = ${patientId} AND p."claimId" IS NULL`,
      ])) as any;

    const totalCount =
      Number(count_claims ?? 0) + Number(count_orphan_payments ?? 0);

    const rawRows = (await db.$queryRaw`
      WITH claim_rows AS (
        SELECT
          'CLAIM'::text AS type,
          c.id,
          COALESCE(c."serviceDate", c."createdAt")::timestamptz AS date,
          c."createdAt"::timestamptz AS created_at,
          c.status::text AS status,
          COALESCE(sum(sl."totalBilled")::numeric::text, '0') AS total_billed,
          COALESCE(sum(sl."totalPaid")::numeric::text, '0') AS total_paid,
          COALESCE(sum(sl."totalAdjusted")::numeric::text, '0') AS total_adjusted,
          COALESCE(sum(sl."totalDue")::numeric::text, '0') AS total_due,
          (
            SELECT (pat."firstName" || ' ' || pat."lastName") FROM "Patient" pat WHERE pat.id = c."patientId" LIMIT 1
          ) AS patient_name,

          -- linked_payment_id (NULL if none). Schema has unique Payment.claimId so LIMIT 1 is safe.
          (
            SELECT p2.id FROM "Payment" p2 WHERE p2."claimId" = c.id LIMIT 1
          ) AS linked_payment_id,

          (
            SELECT coalesce(json_agg(
              json_build_object(
                'id', sl2.id,
                'procedureCode', sl2."procedureCode",
                'procedureDate', sl2."procedureDate",
                'toothNumber', sl2."toothNumber",
                'toothSurface', sl2."toothSurface",
                'totalBilled', sl2."totalBilled",
                'totalPaid', sl2."totalPaid",
                'totalAdjusted', sl2."totalAdjusted",
                'totalDue', sl2."totalDue",
                'status', sl2.status
              )
            ), '[]'::json)
            FROM "ServiceLine" sl2 WHERE sl2."claimId" = c.id
          ) AS service_lines
        FROM "Claim" c
        LEFT JOIN "ServiceLine" sl ON sl."claimId" = c.id
        WHERE c."patientId" = ${patientId}
        GROUP BY c.id
      ),
      orphan_payment_rows AS (
        SELECT
          'PAYMENT'::text AS type,
          p.id,
          p."createdAt"::timestamptz AS date,
          p."createdAt"::timestamptz AS created_at,
          p.status::text AS status,
          p."totalBilled"::numeric::text AS total_billed,
          p."totalPaid"::numeric::text AS total_paid,
          p."totalAdjusted"::numeric::text AS total_adjusted,
          p."totalDue"::numeric::text AS total_due,
          (
            SELECT (pat."firstName" || ' ' || pat."lastName") FROM "Patient" pat WHERE pat.id = p."patientId" LIMIT 1
          ) AS patient_name,

          -- this payment's id is the linked_payment_id
          p.id AS linked_payment_id,

          (
            SELECT coalesce(json_agg(
              json_build_object(
                'id', sl3.id,
                'procedureCode', sl3."procedureCode",
                'procedureDate', sl3."procedureDate",
                'toothNumber', sl3."toothNumber",
                'toothSurface', sl3."toothSurface",
                'totalBilled', sl3."totalBilled",
                'totalPaid', sl3."totalPaid",
                'totalAdjusted', sl3."totalAdjusted",
                'totalDue', sl3."totalDue",
                'status', sl3.status
              )
            ), '[]'::json)
            FROM "ServiceLine" sl3 WHERE sl3."paymentId" = p.id
          ) AS service_lines
        FROM "Payment" p
        WHERE p."patientId" = ${patientId} AND p."claimId" IS NULL
      )
      SELECT type, id, date, created_at, status, total_billed, total_paid, total_adjusted, total_due, patient_name, linked_payment_id, service_lines
      FROM (
        SELECT * FROM claim_rows
        UNION ALL
        SELECT * FROM orphan_payment_rows
      ) t
      ORDER BY t.created_at DESC
      LIMIT ${limit} OFFSET ${offset}
    `) as any[];

    // map to expected JS shape; convert totals to numbers
    const rows: FinancialRow[] = rawRows.map((r: any) => ({
      type: r.type,
      id: Number(r.id),
      date: r.date ? r.date.toString() : null,
      createdAt: r.created_at ? r.created_at.toString() : null,
      status: r.status ?? null,
      total_billed: Number(r.total_billed ?? 0),
      total_paid: Number(r.total_paid ?? 0),
      total_adjusted: Number(r.total_adjusted ?? 0),
      total_due: Number(r.total_due ?? 0),
      patient_name: r.patient_name ?? null,
      service_lines: r.service_lines ?? [],
      linked_payment_id: r.linked_payment_id
        ? Number(r.linked_payment_id)
        : null,
    }));

    return { rows, totalCount };
  } catch (err) {
    console.error("getPatientFinancialRowsFn error:", err);
    throw err;
  }
};
