import { Injectable, NotFoundException } from "@nestjs/common";
import { randomUUID } from "node:crypto";
import { Prisma } from "@jorgecuadros/database";
import { PrismaService } from "../prisma/prisma.service";
import { StorageService } from "../storage/storage.service";
import { extForUpload, type UploadedFileLike } from "../storage/upload-file";
import { toDate } from "../common/coerce";
import { CreatePolicyDto, UpdatePolicyDto } from "./policy.dto";
import {
  BeneficiaryDto,
  ClaimDto,
  DriverDto,
  InstallmentDto,
  UpdateBeneficiaryDto,
  UpdateClaimDto,
  UpdateDriverDto,
  UpdateInstallmentDto,
  VehicleDto,
} from "./children.dto";
import {
  AdjusterDto,
  PolicyTypeDto,
  ProviderDto,
  UpdateAdjusterDto,
  UpdatePolicyTypeDto,
  UpdateProviderDto,
} from "./lookup.dto";

/**
 * Vigencia buckets, derived from `policyTo` against today. `undated` is a real
 * bucket rather than an error case: 528 of the migrated policies carry no end
 * date at all (the legacy Access tables left it blank), so they can neither be
 * called current nor expired.
 */
export type PolicyStatus = "active" | "expiring" | "expired" | "undated";

export type PolicySort =
  | "expiry_desc"
  | "expiry_asc"
  | "customer"
  | "number"
  | "premium_desc";

export interface ListParams {
  query?: string;
  page: number;
  pageSize: number;
  status?: PolicyStatus;
  /** Window in days for the `expiring` bucket. */
  days: number;
  typeId?: string;
  providerId?: string;
  liquidated?: boolean;
  includeArchived?: boolean;
  sort: PolicySort;
}

/** Midnight today, UTC — policy dates are stored date-only at 00:00 UTC. */
function today(): Date {
  const now = new Date();
  return new Date(
    Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()),
  );
}

function addDays(d: Date, days: number): Date {
  return new Date(d.getTime() + days * 86400000);
}

function statusOf(policyTo: Date | null, from: Date, soon: Date): PolicyStatus {
  if (!policyTo) return "undated";
  if (policyTo < from) return "expired";
  return policyTo <= soon ? "expiring" : "active";
}

function daysUntil(policyTo: Date | null, from: Date): number | null {
  if (!policyTo) return null;
  return Math.round((policyTo.getTime() - from.getTime()) / 86400000);
}

@Injectable()
export class PoliciesService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly storage: StorageService,
  ) {}

  private statusWhere(
    status: PolicyStatus | undefined,
    days: number,
  ): Prisma.PolicyWhereInput {
    const from = today();
    switch (status) {
      case "active":
        return { policyTo: { gte: from } };
      case "expiring":
        return { policyTo: { gte: from, lte: addDays(from, days) } };
      case "expired":
        return { policyTo: { lt: from } };
      case "undated":
        return { policyTo: null };
      default:
        return {};
    }
  }

  private orderBy(sort: PolicySort): Prisma.PolicyOrderByWithRelationInput[] {
    switch (sort) {
      case "expiry_asc":
        return [{ policyTo: "asc" }];
      case "customer":
        return [{ customer: { name: "asc" } }, { policyTo: "desc" }];
      case "number":
        return [{ policyNumber: "asc" }];
      case "premium_desc":
        // Sorts on netPremium, not total: `total` is 0 or null on all but 2 of
        // the 2378 migrated policies, so ordering by it is meaningless.
        return [{ netPremium: "desc" }];
      default:
        // MySQL sorts NULLs last on DESC, which puts the 528 undated policies
        // at the end instead of the top — the behaviour we want by default.
        return [{ policyTo: "desc" }];
    }
  }

  /** Policy list with search, vigencia/type/provider filters, paginated. */
  async list(params: ListParams) {
    const { query, page, pageSize, status, days, typeId, providerId, liquidated,
      includeArchived, sort } = params;

    const where: Prisma.PolicyWhereInput = { ...this.statusWhere(status, days) };

    if (!includeArchived) where.archivedAt = null;

    if (query && query.trim()) {
      const q = query.trim();
      where.OR = [
        { policyNumber: { contains: q } },
        { customer: { name: { contains: q } } },
        { agentName: { contains: q } },
        { vehicles: { some: { licensePlate: { contains: q } } } },
        { insuredDrivers: { some: { fullName: { contains: q } } } },
        { legacyId: { contains: q } },
      ];
    }
    if (typeId) where.policyTypeId = typeId;
    if (providerId) where.insuranceProviderId = providerId;
    if (liquidated !== undefined) where.liquidated = liquidated;

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.policy.count({ where }),
      this.prisma.policy.findMany({
        where,
        skip: (page - 1) * pageSize,
        take: pageSize,
        orderBy: this.orderBy(sort),
        select: {
          id: true,
          policyNumber: true,
          agentName: true,
          policyFrom: true,
          policyTo: true,
          netPremium: true,
          total: true,
          currency: true,
          liquidated: true,
          archivedAt: true,
          customer: { select: { id: true, name: true, city: true } },
          policyType: { select: { id: true, name: true } },
          insuranceProvider: { select: { id: true, name: true } },
          _count: { select: { vehicles: true, installments: true, documents: true } },
        },
      }),
    ]);

    const from = today();
    const soon = addDays(from, days);

    const items = rows.map((r) => ({
      id: r.id,
      policyNumber: r.policyNumber,
      agentName: r.agentName,
      policyFrom: r.policyFrom,
      policyTo: r.policyTo,
      netPremium: r.netPremium,
      total: r.total,
      currency: r.currency,
      liquidated: r.liquidated,
      archived: r.archivedAt != null,
      customerId: r.customer.id,
      customerName: r.customer.name,
      customerCity: r.customer.city,
      policyType: r.policyType,
      insuranceProvider: r.insuranceProvider,
      status: statusOf(r.policyTo, from, soon),
      daysToExpiry: daysUntil(r.policyTo, from),
      vehicleCount: r._count.vehicles,
      installmentCount: r._count.installments,
      documentCount: r._count.documents,
    }));

    return { items, total, page, pageSize, pageCount: Math.ceil(total / pageSize) };
  }

  /** Top-line counts for the policies page header. */
  async stats(days: number) {
    const from = today();
    const soon = addDays(from, days);

    const [total, active, expiring, expired, undated, liquidated] =
      await this.prisma.$transaction([
        this.prisma.policy.count(),
        this.prisma.policy.count({ where: { policyTo: { gte: from } } }),
        this.prisma.policy.count({
          where: { policyTo: { gte: from, lte: soon } },
        }),
        this.prisma.policy.count({ where: { policyTo: { lt: from } } }),
        this.prisma.policy.count({ where: { policyTo: null } }),
        this.prisma.policy.count({ where: { liquidated: true } }),
      ]);

    // Premium in force, per currency — the two currencies can't be summed.
    const inForce = await this.prisma.policy.groupBy({
      by: ["currency"],
      where: { policyTo: { gte: from } },
      _sum: { total: true, netPremium: true },
      _count: { _all: true },
    });

    return {
      total,
      active,
      expiring,
      expired,
      undated,
      liquidated,
      pending: total - liquidated,
      days,
      premiumInForce: inForce.map((r) => ({
        currency: r.currency,
        total: r._sum.total,
        netPremium: r._sum.netPremium,
        count: r._count._all,
      })),
    };
  }

  /** Filter dropdown options, with counts so empty choices are visible. */
  async facets() {
    const [types, providers] = await this.prisma.$transaction([
      this.prisma.policyType.findMany({
        orderBy: { name: "asc" },
        select: { id: true, name: true, _count: { select: { policies: true } } },
      }),
      this.prisma.insuranceProvider.findMany({
        orderBy: { name: "asc" },
        select: { id: true, name: true, _count: { select: { policies: true } } },
      }),
    ]);

    return {
      types: types.map((t) => ({ id: t.id, name: t.name, count: t._count.policies })),
      providers: providers.map((p) => ({
        id: p.id,
        name: p.name,
        count: p._count.policies,
      })),
    };
  }

  /** Full policy view, including the owning customer. */
  async detail(id: string, days: number) {
    const policy = await this.prisma.policy.findUnique({
      where: { id },
      include: {
        customer: {
          select: {
            id: true,
            name: true,
            nameSource: true,
            city: true,
            state: true,
            phone: true,
            mobile: true,
            email: true,
          },
        },
        policyType: true,
        insuranceProvider: true,
        installments: { orderBy: { sequence: "asc" } },
        vehicles: true,
        insuredDrivers: true,
        beneficiaries: true,
        claims: { include: { adjuster: true } },
        documents: true,
        properties: {
          select: { id: true, addressLine1: true, addressLine2: true, zone: true },
        },
      },
    });

    if (!policy) {
      throw new NotFoundException(`Policy ${id} not found`);
    }

    const from = today();
    return {
      ...policy,
      status: statusOf(policy.policyTo, from, addDays(from, days)),
      daysToExpiry: daysUntil(policy.policyTo, from),
    };
  }

  // --- policy header writes -------------------------------------------------

  private headerData(dto: CreatePolicyDto | UpdatePolicyDto) {
    const { policyDate, policyFrom, policyTo, liquidationDate, ...rest } =
      dto as CreatePolicyDto;
    return {
      ...rest,
      ...(policyDate !== undefined && { policyDate: toDate(policyDate) }),
      ...(policyFrom !== undefined && { policyFrom: toDate(policyFrom) }),
      ...(policyTo !== undefined && { policyTo: toDate(policyTo) }),
      ...(liquidationDate !== undefined && { liquidationDate: toDate(liquidationDate) }),
    };
  }

  async create(dto: CreatePolicyDto) {
    // Validate the customer FK up front for a clean 404 instead of a raw
    // Prisma constraint error.
    const customer = await this.prisma.customer.findUnique({
      where: { id: dto.customerId },
      select: { id: true },
    });
    if (!customer) throw new NotFoundException(`Customer ${dto.customerId} not found`);

    return this.prisma.policy.create({
      data: {
        ...this.headerData(dto),
        policyNumber: dto.policyNumber,
        customerId: dto.customerId,
      },
    });
  }

  async update(id: string, dto: UpdatePolicyDto) {
    await this.ensurePolicy(id);
    return this.prisma.policy.update({ where: { id }, data: this.headerData(dto) });
  }

  async archive(id: string) {
    await this.ensurePolicy(id);
    return this.prisma.policy.update({ where: { id }, data: { archivedAt: new Date() } });
  }

  async restore(id: string) {
    await this.ensurePolicy(id);
    return this.prisma.policy.update({ where: { id }, data: { archivedAt: null } });
  }


  private async ensurePolicy(id: string) {
    const found = await this.prisma.policy.findUnique({
      where: { id },
      select: { id: true },
    });
    if (!found) throw new NotFoundException(`Policy ${id} not found`);
  }

  // --- child rows -----------------------------------------------------------
  // Each child is created under a policy and edited/removed by its own id,
  // scoped to that policy so one policy's id can't touch another's rows.

  private async ensureChild(
    model: "policyPaymentInstallment" | "vehicle" | "insuredDriver" | "policyBeneficiary" | "claim",
    policyId: string,
    childId: string,
  ) {
    await this.ensurePolicy(policyId);
    // @ts-expect-error dynamic delegate access is safe for these known models
    const row = await this.prisma[model].findFirst({
      where: { id: childId, policyId },
      select: { id: true },
    });
    if (!row) throw new NotFoundException(`Child ${childId} not found on policy ${policyId}`);
  }

  async addInstallment(policyId: string, dto: InstallmentDto) {
    await this.ensurePolicy(policyId);
    return this.prisma.policyPaymentInstallment.create({
      data: {
        policyId,
        sequence: dto.sequence,
        amount: dto.amount,
        currency: dto.currency,
        dueDate: toDate(dto.dueDate) ?? undefined,
        paidDate: toDate(dto.paidDate) ?? undefined,
        checkNumber: dto.checkNumber,
        isCash: dto.isCash,
      },
    });
  }
  async updateInstallment(policyId: string, id: string, dto: UpdateInstallmentDto) {
    await this.ensureChild("policyPaymentInstallment", policyId, id);
    return this.prisma.policyPaymentInstallment.update({
      where: { id },
      data: {
        sequence: dto.sequence,
        amount: dto.amount,
        currency: dto.currency,
        ...(dto.dueDate !== undefined && { dueDate: toDate(dto.dueDate) }),
        ...(dto.paidDate !== undefined && { paidDate: toDate(dto.paidDate) }),
        checkNumber: dto.checkNumber,
        isCash: dto.isCash,
      },
    });
  }
  async removeInstallment(policyId: string, id: string) {
    await this.ensureChild("policyPaymentInstallment", policyId, id);
    return this.prisma.policyPaymentInstallment.delete({ where: { id } });
  }

  async addVehicle(policyId: string, dto: VehicleDto) {
    await this.ensurePolicy(policyId);
    return this.prisma.vehicle.create({ data: { policyId, ...dto } });
  }
  async updateVehicle(policyId: string, id: string, dto: VehicleDto) {
    await this.ensureChild("vehicle", policyId, id);
    return this.prisma.vehicle.update({ where: { id }, data: { ...dto } });
  }
  async removeVehicle(policyId: string, id: string) {
    await this.ensureChild("vehicle", policyId, id);
    return this.prisma.vehicle.delete({ where: { id } });
  }

  async addDriver(policyId: string, dto: DriverDto) {
    await this.ensurePolicy(policyId);
    return this.prisma.insuredDriver.create({
      data: { policyId, ...dto, birthDate: toDate(dto.birthDate) ?? undefined },
    });
  }
  async updateDriver(policyId: string, id: string, dto: UpdateDriverDto) {
    await this.ensureChild("insuredDriver", policyId, id);
    return this.prisma.insuredDriver.update({
      where: { id },
      data: { ...dto, ...(dto.birthDate !== undefined && { birthDate: toDate(dto.birthDate) }) },
    });
  }
  async removeDriver(policyId: string, id: string) {
    await this.ensureChild("insuredDriver", policyId, id);
    return this.prisma.insuredDriver.delete({ where: { id } });
  }

  async addBeneficiary(policyId: string, dto: BeneficiaryDto) {
    await this.ensurePolicy(policyId);
    return this.prisma.policyBeneficiary.create({ data: { policyId, ...dto } });
  }
  async updateBeneficiary(policyId: string, id: string, dto: UpdateBeneficiaryDto) {
    await this.ensureChild("policyBeneficiary", policyId, id);
    return this.prisma.policyBeneficiary.update({ where: { id }, data: { ...dto } });
  }
  async removeBeneficiary(policyId: string, id: string) {
    await this.ensureChild("policyBeneficiary", policyId, id);
    return this.prisma.policyBeneficiary.delete({ where: { id } });
  }

  async addClaim(policyId: string, dto: ClaimDto) {
    await this.ensurePolicy(policyId);
    return this.prisma.claim.create({ data: { policyId, ...this.claimData(dto) } });
  }
  async updateClaim(policyId: string, id: string, dto: UpdateClaimDto) {
    await this.ensureChild("claim", policyId, id);
    return this.prisma.claim.update({ where: { id }, data: this.claimData(dto) });
  }
  async removeClaim(policyId: string, id: string) {
    await this.ensureChild("claim", policyId, id);
    return this.prisma.claim.delete({ where: { id } });
  }
  private claimData(dto: ClaimDto) {
    const { incidentDate, reportedDate, settlementDate, ...rest } = dto;
    return {
      ...rest,
      ...(incidentDate !== undefined && { incidentDate: toDate(incidentDate) }),
      ...(reportedDate !== undefined && { reportedDate: toDate(reportedDate) }),
      ...(settlementDate !== undefined && { settlementDate: toDate(settlementDate) }),
    };
  }

  // --- documents ------------------------------------------------------------
  // Blob in object storage under `policy/<policyId>/…`; row is the pointer.

  async addDocument(
    policyId: string,
    file: UploadedFileLike,
    documentType?: string,
  ) {
    await this.ensurePolicy(policyId);
    const key = `policy/${policyId}/${randomUUID()}${extForUpload(file)}`;
    await this.storage.put(key, file.buffer, file.mimetype);
    return this.prisma.policyDocument.create({
      data: {
        policyId,
        documentType: documentType?.trim() || "DOCUMENT",
        storageKey: key,
      },
    });
  }

  async getDocument(policyId: string, id: string) {
    const row = await this.prisma.policyDocument.findFirst({
      where: { id, policyId },
    });
    if (!row) throw new NotFoundException(`Document ${id} not found on policy ${policyId}`);
    const blob = await this.storage.getStream(row.storageKey);
    return { row, ...blob };
  }

  async removeDocument(policyId: string, id: string) {
    await this.ensurePolicy(policyId);
    const row = await this.prisma.policyDocument.findFirst({
      where: { id, policyId },
      select: { id: true, storageKey: true },
    });
    if (!row) throw new NotFoundException(`Document ${id} not found on policy ${policyId}`);
    const deleted = await this.prisma.policyDocument.delete({ where: { id } });
    await this.storage.delete(row.storageKey);
    return deleted;
  }

  // --- lookups (providers / policy types / adjusters) -----------------------

  listLookups() {
    return this.prisma.$transaction([
      this.prisma.insuranceProvider.findMany({
        orderBy: { name: "asc" },
        select: { id: true, name: true, _count: { select: { policies: true } } },
      }),
      this.prisma.policyType.findMany({
        orderBy: { name: "asc" },
        select: {
          id: true,
          name: true,
          shortDescription: true,
          _count: { select: { policies: true } },
        },
      }),
      this.prisma.adjuster.findMany({ orderBy: { name: "asc" } }),
    ]).then(([providers, types, adjusters]) => ({ providers, types, adjusters }));
  }

  createProvider(dto: ProviderDto) {
    return this.prisma.insuranceProvider.create({ data: dto });
  }
  updateProvider(id: string, dto: UpdateProviderDto) {
    return this.prisma.insuranceProvider.update({ where: { id }, data: dto });
  }
  removeProvider(id: string) {
    return this.prisma.insuranceProvider.delete({ where: { id } });
  }

  createPolicyType(dto: PolicyTypeDto) {
    return this.prisma.policyType.create({ data: dto });
  }
  updatePolicyType(id: string, dto: UpdatePolicyTypeDto) {
    return this.prisma.policyType.update({ where: { id }, data: dto });
  }
  removePolicyType(id: string) {
    return this.prisma.policyType.delete({ where: { id } });
  }

  createAdjuster(dto: AdjusterDto) {
    return this.prisma.adjuster.create({ data: dto });
  }
  updateAdjuster(id: string, dto: UpdateAdjusterDto) {
    return this.prisma.adjuster.update({ where: { id }, data: dto });
  }
  removeAdjuster(id: string) {
    return this.prisma.adjuster.delete({ where: { id } });
  }
}
