import type { Prisma } from '@prisma/client';
import prisma from '../config/db.js';
import type { CreateProjectBody, UpdateProjectBody, AddTeamMemberBody, ProjectStatus } from '../validators/project.validator.js';
import { paginatedResponse, type Pagination } from '../utils/pagination.js';
import { encodeId } from '../utils/idCodec.js';
import { roleLabel } from './session.model.js';
import { userStatus } from './tenantUser.model.js';

// Joi hands back ISO date strings; Prisma's generated types want `Date`.
function toProjectData(body: UpdateProjectBody): Prisma.ProjectUncheckedUpdateInput {
  return {
    ...body,
    startDate: body.startDate ? new Date(body.startDate) : undefined,
    targetCompletionDate: body.targetCompletionDate ? new Date(body.targetCompletionDate) : undefined,
  };
}

export function createProject(data: CreateProjectBody) {
  // `name` is guaranteed present — Joi's createProjectSchema requires it.
  return prisma.project.create({ data: toProjectData(data) as Prisma.ProjectUncheckedCreateInput });
}

export function getProjects({ skip, take }: Pick<Pagination, 'skip' | 'take'>) {
  return prisma.project.findMany({ orderBy: { name: 'asc' }, skip, take });
}

// No organizationId = platform-wide count (SaaS admin's project list); with
// one, scoped to that organization (the tenant admin's "any projects yet?" check).
export function countProjects(organizationId?: number) {
  return prisma.project.count({ where: organizationId ? { organizationId } : undefined });
}

const userSummary = { select: { id: true, name: true, email: true } } as const;

export function getProjectById(id: number) {
  return prisma.project.findUnique({
    where: { id },
    include: {
      projectManager: userSummary,
      planningEngineer: userSummary,
      teamMembers: { include: { user: userSummary }, orderBy: { createdAt: 'asc' } },
      documents: { orderBy: { createdAt: 'desc' } },
    },
  });
}

export function updateProject(id: number, data: UpdateProjectBody) {
  return prisma.project.update({ where: { id }, data: toProjectData(data) });
}

export async function deleteProject(id: number) {
  await prisma.project.delete({ where: { id } });
}

export async function findProjectStatus(id: number) {
  const project = await prisma.project.findUnique({ where: { id }, select: { status: true } });
  return project?.status as ProjectStatus | undefined;
}

export function setProjectStatus(id: number, status: ProjectStatus) {
  return prisma.project.update({ where: { id }, data: { status } });
}

export function logActivity(projectId: number, actorId: number | undefined, action: string, detail?: string) {
  return prisma.projectActivityLog.create({ data: { projectId, actorId, action, detail } });
}

export function getActivityLog(projectId: number, { skip, take }: Pick<Pagination, 'skip' | 'take'>) {
  return prisma.projectActivityLog.findMany({
    where: { projectId },
    include: { actor: userSummary },
    orderBy: { createdAt: 'desc' },
    skip,
    take,
  });
}

export function countActivityLog(projectId: number) {
  return prisma.projectActivityLog.count({ where: { projectId } });
}

export function addTeamMember(projectId: number, data: AddTeamMemberBody) {
  return prisma.projectTeamMember.create({
    data: { projectId, ...data },
    include: { user: userSummary },
  });
}

export function getTeamMembers(projectId: number) {
  return prisma.projectTeamMember.findMany({
    where: { projectId },
    include: { user: userSummary },
    orderBy: { createdAt: 'asc' },
  });
}

export async function removeTeamMember(projectId: number, memberId: number) {
  const member = await prisma.projectTeamMember.findFirst({
    where: { id: memberId, projectId },
    include: { user: userSummary },
  });
  if (!member) return null;
  await prisma.projectTeamMember.delete({ where: { id: memberId } });
  return member;
}

export function createDocument(
  projectId: number,
  file: { fileName: string; filePath: string; fileSize: number; mimeType: string },
  uploadedById: number | undefined,
) {
  return prisma.projectDocument.create({ data: { projectId, ...file, uploadedById } });
}

export function getDocuments(projectId: number) {
  return prisma.projectDocument.findMany({ where: { projectId }, orderBy: { createdAt: 'desc' } });
}

export function getDocumentById(projectId: number, documentId: number) {
  return prisma.projectDocument.findFirst({ where: { id: documentId, projectId } });
}

export async function deleteDocument(projectId: number, documentId: number) {
  const document = await prisma.projectDocument.findFirst({ where: { id: documentId, projectId } });
  if (!document) return null;
  await prisma.projectDocument.delete({ where: { id: documentId } });
  return document;
}

// ── Project access & scope (tenant admin's "Project Access" screen) ──

const projectAccessInclude = {
  sites: { orderBy: { name: 'asc' } },
  projectManager: userSummary,
} satisfies Prisma.ProjectInclude;

type ProjectAccessRecord = Prisma.ProjectGetPayload<{ include: typeof projectAccessInclude }>;

export function projectView(p: ProjectAccessRecord) {
  return {
    id: encodeId(p.id),
    code: p.code,
    name: p.name,
    location: p.location ?? '',
    status: p.status,
    startDate: p.startDate?.toISOString() ?? '',
    targetCompletionDate: p.targetCompletionDate?.toISOString() ?? '',
    progressPct: p.progressPct,
    siteCount: p.sites.length,
    // Towers are modelled as sites until Project Core defines the full hierarchy.
    towerCount: p.sites.length,
    projectManager: p.projectManager
      ? { id: encodeId(p.projectManager.id), name: p.projectManager.name }
      : { id: '', name: '' },
  };
}

/** Projects the user may see: all of them for an "all projects" role, else their memberships. */
export async function visibleProjects(organizationId: number, userId: number, scope: 'all' | 'assigned') {
  const projects = await prisma.project.findMany({
    where: { organizationId, ...(scope === 'assigned' ? { members: { some: { userId } } } : {}) },
    orderBy: { code: 'asc' },
    include: projectAccessInclude,
  });
  return projects.map(projectView);
}

export function findProject(organizationId: number, id: number) {
  return prisma.project.findFirst({ where: { id, organizationId }, include: { sites: true } });
}

export function findProjectsByIds(organizationId: number, ids: number[]) {
  return prisma.project.findMany({ where: { organizationId, id: { in: ids } }, select: { id: true } });
}

export function countMemberships(organizationId: number) {
  return prisma.projectMember.count({ where: { project: { organizationId } } });
}

/** GET /admin/project-access: every project with its sites and members. */
export async function projectAccessView(organizationId: number, projectId?: number) {
  const projects = await prisma.project.findMany({
    where: { organizationId, ...(projectId ? { id: projectId } : {}) },
    orderBy: { code: 'asc' },
    include: {
      sites: { orderBy: { name: 'asc' } },
      members: {
        orderBy: { createdAt: 'asc' },
        include: { user: { include: { tenantRoles: { include: { role: { select: { name: true, label: true } } } } } } },
      },
    },
  });
  return projects.map((p) => ({
    projectId: encodeId(p.id),
    code: p.code,
    name: p.name,
    sites: p.sites.map((s) => ({ id: encodeId(s.id), name: s.name })),
    members: p.members.map((m) => ({
      userId: encodeId(m.userId),
      name: m.user.name,
      email: m.user.email,
      status: userStatus(m.user),
      orgRoles: m.user.tenantRoles.map(({ role }) => roleLabel(role)),
      projectRole: m.projectRole,
      siteIds: m.siteIds.map(encodeId),
      since: m.createdAt.toISOString(),
    })),
  }));
}

/** Adds people to a project, or updates their role/sites if they're already on it. */
export async function upsertMembers(projectId: number, userIds: number[], projectRole: string, siteIds: number[]) {
  await prisma.$transaction(
    userIds.map((userId) =>
      prisma.projectMember.upsert({
        where: { projectId_userId: { projectId, userId } },
        update: { projectRole, siteIds },
        create: { projectId, userId, projectRole, siteIds },
      }),
    ),
  );
}

export function findMember(projectId: number, userId: number) {
  return prisma.projectMember.findUnique({ where: { projectId_userId: { projectId, userId } } });
}

export function updateMember(projectId: number, userId: number, projectRole: string, siteIds: number[]) {
  return prisma.projectMember.update({ where: { projectId_userId: { projectId, userId } }, data: { projectRole, siteIds } });
}

export async function removeMember(projectId: number, userId: number) {
  await prisma.projectMember.delete({ where: { projectId_userId: { projectId, userId } } });
}

// ── Projects & sites setup (tenant admin's "Projects & sites" screen) ──
// The minimal Project Core master Phase 2 needs so Project access has something to
// grant: a name, code, location and the project's sites. The full project flow
// (draft → submitted → approved, team, documents) is Phase 3.

export interface ProjectSetupBody {
  name: string;
  code: string;
  location?: string;
  sites: { id?: string; name: string }[];
}

const setupInclude = {
  sites: { orderBy: { name: 'asc' } },
  _count: { select: { members: true, wbsItems: true } },
} satisfies Prisma.ProjectInclude;

type SetupRecord = Prisma.ProjectGetPayload<{ include: typeof setupInclude }>;

function setupView(p: SetupRecord) {
  return {
    id: encodeId(p.id),
    code: p.code ?? '',
    name: p.name,
    location: p.location ?? '',
    sites: p.sites.map((s) => ({ id: encodeId(s.id), name: s.name })),
    memberCount: p._count.members,
    createdAt: p.createdAt.toISOString(),
  };
}

export interface SetupProjectFilters {
  search?: string;
  /** 'with' = has people assigned, 'without' = nobody assigned yet. */
  people?: 'with' | 'without';
}

/**
 * GET /admin/projects: one page of the projects the caller may set up (every project,
 * or only their own for an "assigned projects" role), searched and filtered on the server.
 */
export async function setupProjects(
  organizationId: number,
  userId: number,
  scope: 'all' | 'assigned',
  filters: SetupProjectFilters,
  page: Pagination,
) {
  const and: Prisma.ProjectWhereInput[] = [{ organizationId }];
  if (scope === 'assigned') and.push({ members: { some: { userId } } });
  if (filters.search) {
    const contains = { contains: filters.search, mode: 'insensitive' as const };
    and.push({ OR: [{ name: contains }, { code: contains }, { location: contains }, { sites: { some: { name: contains } } }] });
  }
  if (filters.people === 'with') and.push({ members: { some: {} } });
  if (filters.people === 'without') and.push({ members: { none: {} } });
  const where = { AND: and };
  const [projects, total] = await Promise.all([
    prisma.project.findMany({ where, orderBy: [{ code: 'asc' }, { id: 'asc' }], include: setupInclude, skip: page.skip, take: page.take }),
    prisma.project.count({ where }),
  ]);
  return paginatedResponse(projects.map(setupView), total, page);
}

export function findSetupProject(organizationId: number, id: number, userId: number, scope: 'all' | 'assigned') {
  return prisma.project.findFirst({
    where: { id, organizationId, ...(scope === 'assigned' ? { members: { some: { userId } } } : {}) },
    include: setupInclude,
  });
}

export function countProjectsInOrganization(organizationId: number) {
  return prisma.project.count({ where: { organizationId } });
}

/** Project codes are unique across the platform (schema), so check before writing. */
export async function projectCodeTaken(code: string, exceptId?: number) {
  const existing = await prisma.project.findUnique({ where: { code }, select: { id: true } });
  return Boolean(existing && existing.id !== exceptId);
}

export async function createSetupProject(
  organizationId: number,
  body: ProjectSetupBody,
  creator?: { userId: number; projectRole: string },
) {
  const project = await prisma.project.create({
    data: {
      organizationId,
      name: body.name,
      code: body.code,
      location: body.location || null,
      sites: { create: body.sites.map(({ name }) => ({ name })) },
      // An "assigned projects" creator would otherwise lose sight of their own project.
      ...(creator ? { members: { create: { userId: creator.userId, projectRole: creator.projectRole } } } : {}),
    },
    include: setupInclude,
  });
  return setupView(project);
}

/**
 * Saves the project and reconciles its sites: listed ids are renamed, new names are
 * added, and missing ones removed. `removedSiteIds` must already be cleared of members.
 */
export async function updateSetupProject(id: number, body: ProjectSetupBody, keep: { id: number; name: string }[], removedSiteIds: number[]) {
  const added = body.sites.filter((s) => !s.id).map(({ name }) => ({ name }));
  const project = await prisma.$transaction(async (tx) => {
    if (removedSiteIds.length) await tx.site.deleteMany({ where: { projectId: id, id: { in: removedSiteIds } } });
    // Rename in two steps so swapping two names can't trip the (project, name) unique index.
    for (const site of keep) await tx.site.update({ where: { id: site.id }, data: { name: `__renaming_${site.id}` } });
    for (const site of keep) await tx.site.update({ where: { id: site.id }, data: { name: site.name } });
    return tx.project.update({
      where: { id },
      data: {
        name: body.name,
        code: body.code,
        location: body.location || null,
        ...(added.length ? { sites: { create: added } } : {}),
      },
      include: setupInclude,
    });
  });
  return setupView(project);
}

/** Members limited to specific sites, per removed site: they'd silently widen to "all sites". */
export async function membersLimitedToSites(projectId: number, siteIds: number[]) {
  if (!siteIds.length) return 0;
  return prisma.projectMember.count({ where: { projectId, siteIds: { hasSome: siteIds } } });
}

export async function deleteSetupProject(id: number) {
  // Sites and memberships cascade with the project.
  await prisma.project.delete({ where: { id } });
}
