import { Injectable, NotFoundException } from "@nestjs/common";
import { randomUUID } from "node:crypto";
import { Prisma, ServiceKind } from "@jorgecuadros/database";
import { PrismaService } from "../prisma/prisma.service";
import { StorageService } from "../storage/storage.service";
import { extForUpload } from "../storage/upload-file";
import { toDate } from "../common/coerce";
import {
  CreatePropertyDto,
  ServiceDto,
  TrustDto,
  UpdatePropertyDto,
  UpdateServiceDto,
} from "./property.dto";

/**
 * Trust (fideicomiso) renewal buckets, derived from `trustAccount.dueDate2`
 * against today. The migration loaded DATMEX's `vence1`/`vence2` pair as
 * `dueDate1`/`dueDate2`; on 531 of the 541 dated trusts `dueDate2` is exactly
 * one year after `dueDate1`, so `dueDate2` is the *next* annual due date — the
 * one staff chase — and `dueDate1` is the period it renewed from.
 *
 * `undated` is a real bucket, not an error: 12 trusts carry no dates at all.
 */
export type TrustStatus = "active" | "expiring" | "expired" | "undated";

/** `with`/`without` filter on the whole property set; the rest are trust buckets. */
export type TrustFilter = "with" | "without" | TrustStatus;

export type PropertySort =
  | "customer"
  | "address"
  | "services_desc"
  | "trust_due_asc"
  | "trust_due_desc";

export interface ListParams {
  query?: string;
  page: number;
  pageSize: number;
  serviceKind?: ServiceKind;
  /** Municipality from the predial service's notes — see `facets()`. */
  municipality?: string;
  bank?: string;
  trust?: TrustFilter;
  /** false = properties with no service rows at all (240 of 1519). */
  hasServices?: boolean;
  customerId?: string;
  /** Window in days for the `expiring` trust bucket. */
  days: number;
  includeArchived?: boolean;
  sort: PropertySort;
}

/** Municipality lives in the predial service's `notes` (939/939 populated,
 *  exactly three values). FEDERAL_ZONE's notes hold the same idea but also
 *  carry non-municipality values like "SUSPENDIDO", so predial is the source. */
const MUNICIPALITY_KIND: ServiceKind = "PROPERTY_TAX";

/** Midnight today, UTC — trust 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 trustStatusOf(
  dueDate: Date | null | undefined,
  from: Date,
  soon: Date,
): TrustStatus {
  if (!dueDate) return "undated";
  if (dueDate < from) return "expired";
  return dueDate <= soon ? "expiring" : "active";
}

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

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

  private trustWhere(
    trust: TrustFilter | undefined,
    days: number,
  ): Prisma.PropertyWhereInput {
    const from = today();
    switch (trust) {
      case "with":
        return { trustAccount: { isNot: null } };
      case "without":
        return { trustAccount: { is: null } };
      case "active":
        return { trustAccount: { dueDate2: { gte: from } } };
      case "expiring":
        return {
          trustAccount: { dueDate2: { gte: from, lte: addDays(from, days) } },
        };
      case "expired":
        return { trustAccount: { dueDate2: { lt: from } } };
      case "undated":
        return { trustAccount: { is: { dueDate2: null } } };
      default:
        return {};
    }
  }

  private orderBy(sort: PropertySort): Prisma.PropertyOrderByWithRelationInput[] {
    switch (sort) {
      case "address":
        return [{ addressLine1: "asc" }, { addressLine2: "asc" }];
      case "services_desc":
        return [{ services: { _count: "desc" } }, { customer: { name: "asc" } }];
      case "trust_due_asc":
        return [{ trustAccount: { dueDate2: "asc" } }];
      case "trust_due_desc":
        return [{ trustAccount: { dueDate2: "desc" } }];
      default:
        // Nameless customers last, same rule the customer list uses.
        return [
          { customer: { nameMissing: "asc" } },
          { customer: { name: "asc" } },
          { addressLine1: "asc" },
        ];
    }
  }

  /** Property list with search, service/trust/municipality filters, paginated. */
  async list(params: ListParams) {
    const {
      query,
      page,
      pageSize,
      serviceKind,
      municipality,
      bank,
      trust,
      hasServices,
      customerId,
      days,
      includeArchived,
      sort,
    } = params;

    const and: Prisma.PropertyWhereInput[] = [this.trustWhere(trust, days)];

    if (!includeArchived) and.push({ archivedAt: null });

    // Sorting by trust due date is only meaningful for properties that have a
    // trust; MySQL would otherwise float the ~966 trust-less rows (NULL first
    // on ASC) above every real due date. Scoping is explicit in the UI label.
    if (sort === "trust_due_asc" || sort === "trust_due_desc") {
      and.push({ trustAccount: { isNot: null } });
    }

    if (query && query.trim()) {
      const q = query.trim();
      and.push({
        OR: [
          { addressLine1: { contains: q } },
          { addressLine2: { contains: q } },
          { phone1: { contains: q } },
          { phone2: { contains: q } },
          { phone3: { contains: q } },
          { zone: { contains: q } },
          { legacyId: { contains: q } },
          { customer: { name: { contains: q } } },
          { services: { some: { accountNumber: { contains: q } } } },
          { services: { some: { meterNumber: { contains: q } } } },
          { trustAccount: { trustNumber: { contains: q } } },
        ],
      });
    }
    if (serviceKind) and.push({ services: { some: { kind: serviceKind } } });
    if (municipality)
      and.push({
        services: { some: { kind: MUNICIPALITY_KIND, notes: municipality } },
      });
    if (bank) and.push({ trustAccount: { bankName: bank } });
    if (hasServices !== undefined)
      and.push(hasServices ? { services: { some: {} } } : { services: { none: {} } });
    if (customerId) and.push({ customerId });

    const where: Prisma.PropertyWhereInput = { AND: and };

    const [total, rows] = await this.prisma.$transaction([
      this.prisma.property.count({ where }),
      this.prisma.property.findMany({
        where,
        skip: (page - 1) * pageSize,
        take: pageSize,
        orderBy: this.orderBy(sort),
        select: {
          id: true,
          addressLine1: true,
          addressLine2: true,
          phone1: true,
          phone2: true,
          phone3: true,
          zone: true,
          archivedAt: true,
          customer: {
            select: { id: true, name: true, city: true, state: true },
          },
          services: {
            select: { id: true, kind: true, active: true, notes: true },
          },
          trustAccount: {
            select: {
              bankName: true,
              trustNumber: true,
              bankFee: true,
              dueDate1: true,
              dueDate2: true,
            },
          },
          _count: { select: { services: true, documents: true } },
        },
      }),
    ]);

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

    const items = rows.map((r) => {
      const predial = r.services.find((s) => s.kind === MUNICIPALITY_KIND);
      return {
        id: r.id,
        addressLine1: r.addressLine1,
        addressLine2: r.addressLine2,
        zone: r.zone,
        archived: r.archivedAt != null,
        phones: [r.phone1, r.phone2, r.phone3].filter(Boolean) as string[],
        customerId: r.customer.id,
        customerName: r.customer.name,
        customerCity: r.customer.city,
        customerState: r.customer.state,
        municipality: predial?.notes ?? null,
        services: r.services.map((s) => ({
          id: s.id,
          kind: s.kind,
          active: s.active,
        })),
        serviceCount: r._count.services,
        activeServiceCount: r.services.filter((s) => s.active).length,
        documentCount: r._count.documents,
        trust: r.trustAccount
          ? {
              bankName: r.trustAccount.bankName,
              trustNumber: r.trustAccount.trustNumber,
              bankFee: r.trustAccount.bankFee,
              dueDate1: r.trustAccount.dueDate1,
              dueDate2: r.trustAccount.dueDate2,
              status: trustStatusOf(r.trustAccount.dueDate2, from, soon),
              daysToDue: daysUntil(r.trustAccount.dueDate2, from),
            }
          : null,
      };
    });

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

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

    const [
      properties,
      owners,
      services,
      withoutServices,
      trusts,
      trustExpiring,
      trustExpired,
      documents,
    ] = await this.prisma.$transaction([
      this.prisma.property.count(),
      this.prisma.customer.count({ where: { properties: { some: {} } } }),
      this.prisma.propertyService.count(),
      this.prisma.property.count({ where: { services: { none: {} } } }),
      this.prisma.property.count({ where: { trustAccount: { isNot: null } } }),
      this.prisma.property.count({
        where: { trustAccount: { dueDate2: { gte: from, lte: soon } } },
      }),
      this.prisma.property.count({
        where: { trustAccount: { dueDate2: { lt: from } } },
      }),
      this.prisma.serviceDocument.count(),
    ]);

    // Service mix, per kind — the operational headline for this line of
    // business (how many bills of each type the office pays every month).
    const byKind = await this.prisma.propertyService.groupBy({
      by: ["kind"],
      _count: { _all: true },
      orderBy: { _count: { kind: "desc" } },
    });

    const activeByKind = await this.prisma.propertyService.groupBy({
      by: ["kind"],
      where: { active: true },
      _count: { _all: true },
    });
    const activeMap = new Map(activeByKind.map((r) => [r.kind, r._count._all]));

    return {
      properties,
      owners,
      services,
      withoutServices,
      trusts,
      trustExpiring,
      trustExpired,
      documents,
      days,
      byKind: byKind.map((r) => ({
        kind: r.kind,
        count: r._count._all,
        active: activeMap.get(r.kind) ?? 0,
      })),
    };
  }

  /** Filter dropdown options, with counts so empty choices are visible. */
  async facets() {
    // Kept as separate awaits rather than one $transaction: Prisma's groupBy
    // result type is lost when the calls are widened into a promise array.
    const kinds = await this.prisma.propertyService.groupBy({
      by: ["kind"],
      _count: { _all: true },
      orderBy: { _count: { kind: "desc" } },
    });
    const municipalities = await this.prisma.propertyService.groupBy({
      by: ["notes"],
      where: { kind: MUNICIPALITY_KIND, notes: { not: null } },
      _count: { _all: true },
      orderBy: { _count: { notes: "desc" } },
    });
    const banks = await this.prisma.trustAccount.groupBy({
      by: ["bankName"],
      where: { bankName: { not: null } },
      _count: { _all: true },
      orderBy: { _count: { bankName: "desc" } },
    });

    return {
      kinds: kinds.map((k) => ({ kind: k.kind, count: k._count._all })),
      municipalities: municipalities.map((m) => ({
        name: m.notes as string,
        count: m._count._all,
      })),
      banks: banks.map((b) => ({
        name: b.bankName as string,
        count: b._count._all,
      })),
    };
  }

  /** Full property view: services, trust, documents, owner and siblings. */
  async detail(id: string, days: number) {
    const property = await this.prisma.property.findUnique({
      where: { id },
      include: {
        customer: {
          select: {
            id: true,
            name: true,
            nameSource: true,
            addressLine1: true,
            city: true,
            state: true,
            phone: true,
            mobile: true,
            email: true,
            _count: { select: { properties: true, policies: true } },
          },
        },
        services: { orderBy: { kind: "asc" } },
        trustAccount: true,
        documents: true,
        policy: {
          select: {
            id: true,
            policyNumber: true,
            policyTo: true,
            policyType: { select: { name: true } },
          },
        },
      },
    });

    if (!property) {
      throw new NotFoundException(`Property ${id} not found`);
    }

    // Other properties of the same owner, so staff can hop between them
    // without going back through the customer file.
    const siblings = await this.prisma.property.findMany({
      where: { customerId: property.customerId, id: { not: id } },
      orderBy: [{ addressLine1: "asc" }],
      select: {
        id: true,
        addressLine1: true,
        addressLine2: true,
        zone: true,
        _count: { select: { services: true } },
      },
    });

    // Utility-domain ledger for the OWNER, not for this property: the legacy
    // data ties payments to the customer, never to a specific property, so
    // these are shown as the customer's service movements.
    const transactions = await this.prisma.transaction.findMany({
      where: { customerId: property.customerId, domain: "UTILITY" },
      orderBy: { transactionDate: "desc" },
      take: 12,
      include: { type: true },
    });
    const ledger = await this.prisma.transaction.groupBy({
      by: ["currency"],
      where: { customerId: property.customerId, domain: "UTILITY", voidedAt: null },
      _sum: { amount: true },
      _count: { _all: true },
    });

    const from = today();
    const predial = property.services.find((s) => s.kind === MUNICIPALITY_KIND);

    return {
      ...property,
      municipality: predial?.notes ?? null,
      trustStatus: trustStatusOf(
        property.trustAccount?.dueDate2,
        from,
        addDays(from, days),
      ),
      daysToTrustDue: daysUntil(property.trustAccount?.dueDate2, from),
      siblings: siblings.map((s) => ({
        id: s.id,
        addressLine1: s.addressLine1,
        addressLine2: s.addressLine2,
        zone: s.zone,
        serviceCount: s._count.services,
      })),
      customerTransactions: transactions,
      customerLedger: ledger.map((l) => ({
        currency: l.currency,
        total: l._sum.amount,
        count: l._count._all,
      })),
    };
  }

  // --- property header writes -----------------------------------------------

  async create(dto: CreatePropertyDto) {
    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.property.create({ data: { ...dto } });
  }

  async update(id: string, dto: UpdatePropertyDto) {
    await this.ensureProperty(id);
    return this.prisma.property.update({ where: { id }, data: { ...dto } });
  }

  async archive(id: string) {
    await this.ensureProperty(id);
    return this.prisma.property.update({ where: { id }, data: { archivedAt: new Date() } });
  }
  async restore(id: string) {
    await this.ensureProperty(id);
    return this.prisma.property.update({ where: { id }, data: { archivedAt: null } });
  }

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

  private async ensureService(propertyId: string, serviceId: string) {
    await this.ensureProperty(propertyId);
    const row = await this.prisma.propertyService.findFirst({
      where: { id: serviceId, propertyId },
      select: { id: true },
    });
    if (!row) throw new NotFoundException(`Service ${serviceId} not found on property ${propertyId}`);
  }

  // --- services -------------------------------------------------------------

  async addService(propertyId: string, dto: ServiceDto) {
    await this.ensureProperty(propertyId);
    return this.prisma.propertyService.create({ data: { propertyId, ...dto } });
  }
  async updateService(propertyId: string, id: string, dto: UpdateServiceDto) {
    await this.ensureService(propertyId, id);
    return this.prisma.propertyService.update({ where: { id }, data: { ...dto } });
  }
  async removeService(propertyId: string, id: string) {
    await this.ensureService(propertyId, id);
    return this.prisma.propertyService.delete({ where: { id } });
  }

  // --- trust account (1:1 upsert) -------------------------------------------

  async upsertTrust(propertyId: string, dto: TrustDto) {
    await this.ensureProperty(propertyId);
    const data = {
      bankName: dto.bankName,
      trustNumber: dto.trustNumber,
      bankFee: dto.bankFee,
      ...(dto.dueDate1 !== undefined && { dueDate1: toDate(dto.dueDate1) }),
      ...(dto.dueDate2 !== undefined && { dueDate2: toDate(dto.dueDate2) }),
    };
    return this.prisma.trustAccount.upsert({
      where: { propertyId },
      create: { propertyId, ...data },
      update: data,
    });
  }
  async removeTrust(propertyId: string) {
    await this.ensureProperty(propertyId);
    const existing = await this.prisma.trustAccount.findUnique({
      where: { propertyId },
      select: { id: true },
    });
    if (!existing) throw new NotFoundException(`No trust account on property ${propertyId}`);
    return this.prisma.trustAccount.delete({ where: { propertyId } });
  }

  // --- documents ------------------------------------------------------------
  // The blob lives in object storage (MinIO); the row is just the pointer. Keys
  // stay under the `service/<propertyId>/…` prefix the migration established.

  async addDocument(
    propertyId: string,
    file: { buffer: Buffer; originalname?: string; mimetype?: string },
    documentType?: string,
  ) {
    await this.ensureProperty(propertyId);
    const ext = extForUpload(file);
    const key = `service/${propertyId}/${randomUUID()}${ext}`;
    await this.storage.put(key, file.buffer, file.mimetype);
    return this.prisma.serviceDocument.create({
      data: {
        propertyId,
        documentType: documentType?.trim() || "DOCUMENT",
        storageKey: key,
      },
    });
  }

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

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