import { and, eq, count, inArray, isNull, desc, or, sql } from 'drizzle-orm';

import { db } from '@/lib/db';
import { clients } from '@/lib/db/schema/clients';
import { interests } from '@/lib/db/schema/interests';
import { getPrimaryBerthsForInterests } from '@/lib/services/interest-berths.service';
import { documents, files } from '@/lib/db/schema/documents';
import { invoices } from '@/lib/db/schema/financial';
import { berths } from '@/lib/db/schema/berths';
import { ports } from '@/lib/db/schema/ports';
import { yachts } from '@/lib/db/schema/yachts';
import { companies, companyMemberships } from '@/lib/db/schema/companies';
import { berthTenancies } from '@/lib/db/schema/tenancies';
import { presignDownloadUrl } from '@/lib/storage';
import { getCountryName } from '@/lib/i18n/countries';

// ─── Dashboard ────────────────────────────────────────────────────────────────

export interface PortalDashboard {
  client: {
    id: string;
    fullName: string;
    nationality: string | null;
  };
  port: {
    name: string;
    logoUrl: string | null;
  };
  counts: {
    interests: number;
    documents: number;
    invoices: number;
    yachts: number;
    memberships: number;
    activeTenancies: number;
  };
}

export async function getPortalDashboard(
  clientId: string,
  portId: string,
): Promise<PortalDashboard | null> {
  const [client, port, interestCount, documentCount, yachtList, membershipList, tenancyList] =
    await Promise.all([
      db.query.clients.findFirst({
        where: and(eq(clients.id, clientId), eq(clients.portId, portId)),
        with: { contacts: true },
      }),
      db.query.ports.findFirst({
        where: eq(ports.id, portId),
      }),
      db
        .select({ value: count() })
        .from(interests)
        .where(and(eq(interests.clientId, clientId), eq(interests.portId, portId))),
      db
        .select({ value: count() })
        .from(documents)
        .where(and(eq(documents.clientId, clientId), eq(documents.portId, portId))),
      getPortalUserYachts(clientId, portId),
      getPortalUserMemberships(clientId, portId),
      getPortalUserTenancies(clientId, portId),
    ]);

  if (!client || !port) return null;

  // Count invoices matched by client's billing email addresses
  const emailContacts = (client.contacts ?? [])
    .filter((c) => c.channel === 'email')
    .map((c) => c.value.toLowerCase());

  let invoiceCount = 0;
  if (emailContacts.length > 0) {
    const allPortInvoices = await db
      .select({ billingEmail: invoices.billingEmail })
      .from(invoices)
      .where(eq(invoices.portId, portId));
    invoiceCount = allPortInvoices.filter(
      (inv) => inv.billingEmail && emailContacts.includes(inv.billingEmail.toLowerCase()),
    ).length;
  }

  return {
    client: {
      id: client.id,
      fullName: client.fullName,
      nationality: client.nationalityIso ? getCountryName(client.nationalityIso, 'en') : null,
    },
    port: {
      name: port.name,
      logoUrl: port.logoUrl ?? null,
    },
    counts: {
      interests: interestCount[0]?.value ?? 0,
      documents: documentCount[0]?.value ?? 0,
      invoices: invoiceCount,
      yachts: yachtList.length,
      memberships: membershipList.length,
      activeTenancies: tenancyList.length,
    },
  };
}

// ─── Interests ────────────────────────────────────────────────────────────────

export interface PortalInterest {
  id: string;
  pipelineStage: string;
  leadCategory: string | null;
  berthMooringNumber: string | null;
  berthArea: string | null;
  eoiStatus: string | null;
  contractStatus: string | null;
  dateFirstContact: Date | null;
  createdAt: Date;
}

export async function getClientInterests(
  clientId: string,
  portId: string,
): Promise<PortalInterest[]> {
  const rows = await db
    .select({
      id: interests.id,
      pipelineStage: interests.pipelineStage,
      leadCategory: interests.leadCategory,
      eoiStatus: interests.eoiStatus,
      contractStatus: interests.contractStatus,
      dateFirstContact: interests.dateFirstContact,
      createdAt: interests.createdAt,
    })
    .from(interests)
    .where(and(eq(interests.clientId, clientId), eq(interests.portId, portId)))
    .orderBy(interests.createdAt);

  // Resolve each interest's primary berth via the junction (plan §3.4) -
  // single round-trip for the whole list.
  const primaryBerthMap = await getPrimaryBerthsForInterests(rows.map((r) => r.id));
  const primaryBerthIds = Array.from(
    new Set(Array.from(primaryBerthMap.values(), (b) => b.berthId)),
  );

  const berthMap = new Map<string, { mooringNumber: string; area: string | null }>();
  if (primaryBerthIds.length > 0) {
    const berthRows = await db
      .select({ id: berths.id, mooringNumber: berths.mooringNumber, area: berths.area })
      .from(berths)
      .where(and(eq(berths.portId, portId), inArray(berths.id, primaryBerthIds)));
    for (const b of berthRows) {
      berthMap.set(b.id, { mooringNumber: b.mooringNumber, area: b.area });
    }
  }

  return rows.map((r) => {
    const primary = primaryBerthMap.get(r.id);
    const berthMeta = primary ? (berthMap.get(primary.berthId) ?? null) : null;
    return {
      id: r.id,
      pipelineStage: r.pipelineStage,
      leadCategory: r.leadCategory,
      berthMooringNumber: berthMeta?.mooringNumber ?? null,
      berthArea: berthMeta?.area ?? null,
      eoiStatus: r.eoiStatus,
      contractStatus: r.contractStatus,
      dateFirstContact: r.dateFirstContact,
      createdAt: r.createdAt,
    };
  });
}

// ─── Documents ────────────────────────────────────────────────────────────────

export interface PortalDocument {
  id: string;
  documentType: string;
  title: string;
  status: string;
  isManualUpload: boolean;
  hasSignedFile: boolean;
  signers: Array<{
    signerName: string;
    signerEmail: string;
    signerRole: string;
    status: string;
  }>;
  createdAt: Date;
}

export async function getClientDocuments(
  clientId: string,
  portId: string,
): Promise<PortalDocument[]> {
  const rows = await db.query.documents.findMany({
    where: and(eq(documents.clientId, clientId), eq(documents.portId, portId)),
    with: {
      signers: true,
    },
    orderBy: (docs, { desc }) => [desc(docs.createdAt)],
  });

  return rows.map((doc) => ({
    id: doc.id,
    documentType: doc.documentType,
    title: doc.title,
    status: doc.status,
    isManualUpload: doc.isManualUpload,
    hasSignedFile: doc.signedFileId != null,
    signers: (doc.signers ?? []).map((s) => ({
      signerName: s.signerName,
      signerEmail: s.signerEmail,
      signerRole: s.signerRole,
      status: s.status,
    })),
    createdAt: doc.createdAt,
  }));
}

// ─── Invoices ─────────────────────────────────────────────────────────────────

export interface PortalInvoice {
  id: string;
  invoiceNumber: string;
  status: string;
  currency: string;
  total: string;
  dueDate: string;
  paymentStatus: string | null;
  paymentDate: string | null;
  createdAt: Date;
}

export async function getClientInvoices(
  clientId: string,
  portId: string,
): Promise<PortalInvoice[]> {
  // Look up the client to get billing email for invoice matching
  const client = await db.query.clients.findFirst({
    where: and(eq(clients.id, clientId), eq(clients.portId, portId)),
    with: {
      contacts: true,
    },
  });

  if (!client) return [];

  // Get client's email addresses to match against billingEmail
  const emailContacts = (client.contacts ?? [])
    .filter((c) => c.channel === 'email')
    .map((c) => c.value.toLowerCase());

  // G-I5: the most common B2B pattern is "individual client buys through their
  // company" - those invoices ship with billingEntityType='company' and the
  // portal user (client) is just a director of that company. Filtering on
  // billingEmail alone hides these invoices. Resolve director memberships
  // through company_memberships (role='director', active = endDate IS NULL)
  // and OR them into the predicate.
  const directorMemberships = await db
    .select({ companyId: companyMemberships.companyId })
    .from(companyMemberships)
    .innerJoin(companies, eq(companyMemberships.companyId, companies.id))
    .where(
      and(
        eq(companyMemberships.clientId, clientId),
        eq(companyMemberships.role, 'director'),
        isNull(companyMemberships.endDate),
        eq(companies.portId, portId),
      ),
    );
  const directorCompanyIds = directorMemberships.map((m) => m.companyId);

  // If the portal user has neither billing emails on file nor any active
  // director memberships, there's nothing this query could return.
  if (emailContacts.length === 0 && directorCompanyIds.length === 0) return [];

  // Build the OR predicate: (billingEmail ∈ client emails) OR
  // (billingEntityType='company' AND billingEntityId ∈ director company ids).
  const emailPredicate =
    emailContacts.length > 0
      ? inArray(sql`lower(${invoices.billingEmail})`, emailContacts)
      : undefined;
  const companyPredicate =
    directorCompanyIds.length > 0
      ? and(
          eq(invoices.billingEntityType, 'company'),
          inArray(invoices.billingEntityId, directorCompanyIds),
        )
      : undefined;
  const matchPredicate =
    emailPredicate && companyPredicate
      ? or(emailPredicate, companyPredicate)
      : (emailPredicate ?? companyPredicate);

  // Fetch only the invoices matching any of the client's email addresses or
  // company memberships. Without the predicate push-down here every portal
  // invoice page-load full-scanned the invoices table and filtered in JS -
  // by 12mo it would have been the worst portal endpoint in the platform.
  // Defensive limit 100 caps the upper bound for clients with abnormally many
  // invoices.
  const clientInvoices = await db
    .select()
    .from(invoices)
    .where(and(eq(invoices.portId, portId), matchPredicate))
    .orderBy(invoices.createdAt)
    .limit(100);

  return clientInvoices.map((inv) => ({
    id: inv.id,
    invoiceNumber: inv.invoiceNumber,
    status: inv.status,
    currency: inv.currency,
    total: inv.total,
    dueDate: inv.dueDate,
    paymentStatus: inv.paymentStatus ?? null,
    paymentDate: inv.paymentDate ?? null,
    createdAt: inv.createdAt,
  }));
}

// ─── Document Download ────────────────────────────────────────────────────────

export async function getDocumentDownloadUrl(
  clientId: string,
  documentId: string,
  portId: string,
): Promise<string | null> {
  const doc = await db.query.documents.findFirst({
    where: and(
      eq(documents.id, documentId),
      eq(documents.clientId, clientId),
      eq(documents.portId, portId),
    ),
  });

  if (!doc) return null;

  // Prefer signed file, fall back to original file
  const fileId = doc.signedFileId ?? doc.fileId;
  if (!fileId) return null;

  // Defense-in-depth: scope the file lookup to the same port. Today the
  // doc → file relationship is tightly coupled (signedFileId/fileId are
  // assigned in same-port flows), but a future Documenso-webhook bug or
  // bulk-import drift could leave a foreign-port file id on a doc and
  // this presign call would otherwise return a URL to those bytes.
  const file = await db.query.files.findFirst({
    where: and(eq(files.id, fileId), eq(files.portId, portId)),
  });

  if (!file) return null;

  // M-IN01: 4-hour TTL for portal links so clients clicking on a saved
  // email don't see "link expired" after 15 minutes. Storage backend's
  // default is 900s - that's appropriate for in-CRM previews but
  // hostile for end-clients who may revisit a doc the next morning.
  return presignDownloadUrl(file.storagePath, 4 * 3600);
}

// ─── Yachts (direct + via company) ────────────────────────────────────────────

export interface PortalYacht {
  id: string;
  name: string;
  hullNumber: string | null;
  registration: string | null;
  flag: string | null;
  yearBuilt: number | null;
  lengthFt: string | null;
  widthFt: string | null;
  status: string;
  ownerContext: 'direct' | 'company';
  ownerCompanyId: string | null;
  ownerCompanyName: string | null;
}

function toPortalYacht(y: typeof yachts.$inferSelect): PortalYacht {
  return {
    id: y.id,
    name: y.name,
    hullNumber: y.hullNumber,
    registration: y.registration,
    flag: y.flag,
    yearBuilt: y.yearBuilt,
    lengthFt: y.lengthFt,
    widthFt: y.widthFt,
    status: y.status,
    ownerContext: 'direct',
    ownerCompanyId: null,
    ownerCompanyName: null,
  };
}

export async function getPortalUserYachts(
  clientId: string,
  portId: string,
): Promise<PortalYacht[]> {
  // 1. Direct yachts
  const directYachts = await db.query.yachts.findMany({
    where: and(
      eq(yachts.portId, portId),
      eq(yachts.currentOwnerType, 'client'),
      eq(yachts.currentOwnerId, clientId),
      isNull(yachts.archivedAt),
    ),
  });

  // 2. Active company memberships
  const memberships = await db
    .select({ companyId: companyMemberships.companyId })
    .from(companyMemberships)
    .innerJoin(companies, eq(companyMemberships.companyId, companies.id))
    .where(
      and(
        eq(companyMemberships.clientId, clientId),
        eq(companies.portId, portId),
        isNull(companyMemberships.endDate),
      ),
    );

  const companyIds = memberships.map((m) => m.companyId);

  // De-dup by yacht.id
  const seen = new Set<string>();
  const result: PortalYacht[] = [];

  for (const y of directYachts) {
    if (seen.has(y.id)) continue;
    seen.add(y.id);
    result.push(toPortalYacht(y));
  }

  if (companyIds.length === 0) {
    return result;
  }

  // 3. Company-owned yachts
  const companyYachts = await db
    .select({
      yacht: yachts,
      company: { id: companies.id, name: companies.name },
    })
    .from(yachts)
    .innerJoin(companies, eq(yachts.currentOwnerId, companies.id))
    .where(
      and(
        eq(yachts.portId, portId),
        eq(yachts.currentOwnerType, 'company'),
        inArray(yachts.currentOwnerId, companyIds),
        isNull(yachts.archivedAt),
      ),
    );

  for (const row of companyYachts) {
    if (seen.has(row.yacht.id)) continue;
    seen.add(row.yacht.id);
    result.push({
      id: row.yacht.id,
      name: row.yacht.name,
      hullNumber: row.yacht.hullNumber,
      registration: row.yacht.registration,
      flag: row.yacht.flag,
      yearBuilt: row.yacht.yearBuilt,
      lengthFt: row.yacht.lengthFt,
      widthFt: row.yacht.widthFt,
      status: row.yacht.status,
      ownerContext: 'company',
      ownerCompanyId: row.company.id,
      ownerCompanyName: row.company.name,
    });
  }

  return result;
}

// ─── Memberships ──────────────────────────────────────────────────────────────

export interface PortalMembership {
  membershipId: string;
  role: string;
  isPrimary: boolean;
  startDate: Date;
  company: {
    id: string;
    name: string;
    legalName: string | null;
    status: string;
  };
}

export async function getPortalUserMemberships(
  clientId: string,
  portId: string,
): Promise<PortalMembership[]> {
  const rows = await db
    .select({
      membershipId: companyMemberships.id,
      role: companyMemberships.role,
      isPrimary: companyMemberships.isPrimary,
      startDate: companyMemberships.startDate,
      company: {
        id: companies.id,
        name: companies.name,
        legalName: companies.legalName,
        status: companies.status,
      },
    })
    .from(companyMemberships)
    .innerJoin(companies, eq(companyMemberships.companyId, companies.id))
    .where(
      and(
        eq(companyMemberships.clientId, clientId),
        eq(companies.portId, portId),
        isNull(companyMemberships.endDate),
      ),
    );
  return rows;
}

// ─── Tenancies ────────────────────────────────────────────────────────────────

export interface PortalTenancy {
  id: string;
  berthId: string;
  berthMooringNumber: string | null;
  yachtId: string;
  yachtName: string | null;
  status: 'pending' | 'active' | 'ended' | 'cancelled';
  startDate: Date;
  endDate: Date | null;
  tenureType: string;
}

export async function getPortalUserTenancies(
  clientId: string,
  portId: string,
): Promise<PortalTenancy[]> {
  const rows = await db
    .select({
      id: berthTenancies.id,
      berthId: berthTenancies.berthId,
      berthMooringNumber: berths.mooringNumber,
      yachtId: berthTenancies.yachtId,
      yachtName: yachts.name,
      status: berthTenancies.status,
      startDate: berthTenancies.startDate,
      endDate: berthTenancies.endDate,
      tenureType: berthTenancies.tenureType,
    })
    .from(berthTenancies)
    .innerJoin(berths, eq(berthTenancies.berthId, berths.id))
    .innerJoin(yachts, eq(berthTenancies.yachtId, yachts.id))
    .where(
      and(
        eq(berthTenancies.clientId, clientId),
        eq(berthTenancies.portId, portId),
        inArray(berthTenancies.status, ['pending', 'active']),
      ),
    )
    .orderBy(desc(berthTenancies.createdAt));
  return rows as PortalTenancy[];
}
