import prisma from '../config/db.js';
import type {
  CreateOrganizationBody, UpdateOrganizationBody, UpdateStatusBody, UpdateSubscriptionBody, ChangeTenantAdminBody,
} from '../validators/organization.validator.js';
import { CORE_MODULE_KEYS } from '../config/modules.js';
import { ORG_ADMIN_ROLE } from '../config/access.js';
import { fileUrl } from '../utils/storage.js';
import * as inviteModel from './organizationInvite.model.js';
import * as userModel from './user.model.js';
import { logAudit, type AuditChange } from './auditLog.model.js';

export interface OrganizationListFilter {
  search?: string;
  status?: string;
  planId?: string;
}

const withPlan = { plan: true } as const;

async function computeUsage(organizationId: number) {
  const [userCount, projectCount] = await Promise.all([
    prisma.user.count({ where: { organizationId } }),
    prisma.project.count({ where: { organizationId } }),
  ]);
  // Storage isn't tracked anywhere yet (no file-size aggregation across an
  // org's projects), so this is always 0 until that's built.
  return { maxUsers: userCount, maxProjects: projectCount, storageGb: 0 };
}

// Best-effort snapshot for the `tenantAdmin` field — derived from the most
// recent invite (the only path the onboarding wizard itself creates).
// `changeTenantAdmin(mode: 'existing')` doesn't create an invite, so an org
// reassigned that way falls back to null here — a known gap, not a bug.
export async function getTenantAdminSnapshot(organizationId: number) {
  const invite = await prisma.organizationInvite.findFirst({
    where: { organizationId },
    orderBy: { invitedAt: 'desc' },
    include: { createdUser: true },
  });
  if (!invite) {
    // Organizations set up outside onboarding (seed, db:assign-user) have no invite:
    // fall back to their first active Organization Admin, as the tenant app does.
    const admin = await prisma.user.findFirst({
      where: {
        organizationId,
        isActive: true,
        firebaseUid: { not: null },
        tenantRoles: { some: { role: { name: ORG_ADMIN_ROLE } } },
      },
      orderBy: { id: 'asc' },
    });
    return admin
      ? {
          name: admin.name, email: admin.email, status: 'active' as const, invitedAt: admin.createdAt,
          lastActiveAt: admin.lastLoginAt, avatarUrl: await fileUrl(admin.avatarUrl) ?? null,
        }
      : null;
  }
  if (invite.status === 'accepted' && invite.createdUser) {
    return {
      name: invite.createdUser.name, email: invite.createdUser.email, status: 'active' as const,
      invitedAt: invite.invitedAt, lastActiveAt: invite.createdUser.lastLoginAt,
      // Their profile photo (S3 key or a Firebase/Google photo URL), as a URL to show.
      avatarUrl: await fileUrl(invite.createdUser.avatarUrl) ?? null,
    };
  }
  // Super Admin can copy the link and share it by hand, e.g. when the email doesn't arrive.
  // Only a usable invite gets one: pending and not expired.
  const usable = inviteModel.isInviteUsable(invite);
  return {
    name: invite.name, email: invite.email, status: 'invited' as const, invitedAt: invite.invitedAt,
    ...(usable ? { inviteUrl: inviteModel.inviteUrl(invite.token), inviteExpiresAt: invite.expiresAt } : {}),
  };
}

type OrgRow = {
  id: number; name: string; slug: string; logoUrl: string | null; legalName: string | null; contactEmail: string; contactPhone: string | null;
  country: string; city: string | null; description: string | null; status: string; planId: string | null;
  moduleKeys: string[]; maxUsers: number; maxProjects: number; storageGb: number; subscriptionStatus: string;
  priceMonthly: number; currency: string; subscriptionStartsAt: Date | null; subscriptionRenewsAt: Date | null;
  suspensionReason: string | null; suspensionNote: string | null; suspendedAt: Date | null; createdAt: Date;
  updatedAt: Date; plan: { name: string } | null;
};

async function toApiShape(org: OrgRow, usage: { maxUsers: number; maxProjects: number; storageGb: number }) {
  const {
    maxUsers, maxProjects, storageGb, plan, subscriptionStatus, priceMonthly, currency,
    subscriptionStartsAt, subscriptionRenewsAt, suspensionReason, suspensionNote, suspendedAt, ...rest
  } = org;
  return {
    ...rest,
    // The tenant uploads its logo (S3 key, or an older pasted URL); Super Admin gets a URL to show.
    logoUrl: await fileUrl(org.logoUrl) ?? null,
    planName: plan?.name ?? null,
    limits: { maxUsers, maxProjects, storageGb },
    usage,
    tenantAdmin: await getTenantAdminSnapshot(org.id),
    subscription: {
      status: subscriptionStatus, priceMonthly, currency,
      startsAt: subscriptionStartsAt, renewsAt: subscriptionRenewsAt,
    },
    suspension: org.status === 'suspended' ? { reason: suspensionReason, note: suspensionNote, at: suspendedAt } : undefined,
  };
}

export async function getOrganizationAdminRoleId() {
  const role = await prisma.role.findUnique({ where: { name: 'organization-admin' } });
  return role?.id;
}

export async function createOrganization(body: CreateOrganizationBody, invitedById: number | undefined) {
  const plan = await prisma.plan.findUnique({ where: { id: body.planId } });
  const settings = await prisma.platformSettings.upsert({ where: { id: 1 }, update: {}, create: { id: 1 } });
  const moduleKeys = Array.from(new Set([...CORE_MODULE_KEYS, ...body.moduleKeys]));
  const now = new Date();
  const status = body.activateNow ? 'active' : 'trial';
  const subscriptionStatus = body.activateNow ? 'active' : 'trialing';
  const renewsInDays = body.activateNow ? 30 : settings.defaultTrialDays;
  const subscriptionRenewsAt = new Date(now.getTime() + renewsInDays * 24 * 60 * 60 * 1000);

  const organization = await prisma.organization.create({
    data: {
      name: body.name,
      slug: body.slug,
      legalName: body.legalName,
      contactEmail: body.contactEmail,
      contactPhone: body.contactPhone,
      country: body.country,
      city: body.city,
      planId: body.planId,
      moduleKeys,
      maxUsers: body.limits.maxUsers,
      maxProjects: body.limits.maxProjects,
      storageGb: body.limits.storageGb,
      status,
      subscriptionStatus,
      priceMonthly: plan?.priceMonthly ?? 0,
      currency: plan?.currency ?? 'INR',
      subscriptionStartsAt: now,
      subscriptionRenewsAt,
    },
    include: withPlan,
  });

  const invite = await inviteModel.createInvite(organization.id, { name: body.tenantAdmin.name, email: body.tenantAdmin.email }, invitedById);
  await inviteModel.sendOrganizationAdminInviteEmail(invite, organization.name);

  await logAudit({
    target: { type: 'organization', id: String(organization.id), label: organization.name },
    actorId: invitedById,
    action: 'organization.created',
  });

  return { organization: await toApiShape(organization, { maxUsers: 0, maxProjects: 0, storageGb: 0 }), invite };
}

export async function getOrganizations({ skip, take }: { skip: number; take: number }, filter: OrganizationListFilter) {
  const where = {
    ...(filter.status ? { status: filter.status } : {}),
    ...(filter.planId ? { planId: filter.planId } : {}),
    ...(filter.search
      ? { OR: [{ name: { contains: filter.search, mode: 'insensitive' as const } }, { slug: { contains: filter.search, mode: 'insensitive' as const } }] }
      : {}),
  };

  const [organizations, total, statusGroups] = await Promise.all([
    prisma.organization.findMany({ where, orderBy: { name: 'asc' }, skip, take, include: withPlan }),
    prisma.organization.count({ where }),
    prisma.organization.groupBy({ by: ['status'], _count: true, where: filter.search ? { OR: where.OR } : undefined }),
  ]);

  const items = await Promise.all(organizations.map(async (org) => toApiShape(org, await computeUsage(org.id))));
  const statusCounts: Record<string, number> = { all: 0, pending: 0, trial: 0, active: 0, suspended: 0 };
  for (const group of statusGroups) {
    statusCounts[group.status] = group._count;
    statusCounts.all += group._count;
  }
  return { items, total, statusCounts };
}

export async function getOrganizationById(id: number) {
  const organization = await prisma.organization.findUnique({ where: { id }, include: withPlan });
  if (!organization) return null;
  return toApiShape(organization, await computeUsage(id));
}

// Matches blenaxis-super-admin's OrganizationUser type exactly: `roles`
// (plural array — our system is single-role-per-user today, so always one
// entry) and a derived `status`, not the raw `isActive`/`role` shape the
// rest of this backend uses internally.
export async function getOrganizationUsers(organizationId: number) {
  const users = await prisma.user.findMany({ where: { organizationId }, include: { role: true }, orderBy: { name: 'asc' } });
  return Promise.all(users.map(async (user) => ({
    id: user.id,
    name: user.name,
    email: user.email,
    roles: [user.role?.name ?? 'user'],
    status: !user.isActive ? 'disabled' : !user.lastLoginAt ? 'invited' : 'active',
    lastActiveAt: user.lastLoginAt,
    avatarUrl: await fileUrl(user.avatarUrl) ?? null,
  })));
}

export async function updateOrganization(id: number, data: UpdateOrganizationBody) {
  const organization = await prisma.organization.update({ where: { id }, data, include: withPlan });
  return toApiShape(organization, await computeUsage(id));
}

export async function updateStatus(id: number, body: UpdateStatusBody, actorId: number | undefined) {
  const before = await prisma.organization.findUnique({ where: { id }, select: { status: true } });
  const data =
    body.status === 'suspended'
      ? { status: 'suspended', isActive: false, suspensionReason: body.reason, suspensionNote: body.note, suspendedAt: new Date() }
      : { status: 'active', isActive: true, suspensionReason: null, suspensionNote: null, suspendedAt: null };

  const organization = await prisma.organization.update({ where: { id }, data, include: withPlan });
  await logAudit({
    target: { type: 'organization', id: String(id), label: organization.name },
    actorId,
    action: body.status === 'suspended' ? 'organization.suspended' : 'organization.activated',
    reason: body.status === 'suspended' ? body.reason : undefined,
    changes: [{ field: 'status', before: before?.status ?? '', after: organization.status }],
  });
  return toApiShape(organization, await computeUsage(id));
}

export async function updateSubscription(id: number, body: UpdateSubscriptionBody, actorId: number | undefined) {
  const before = await prisma.organization.findUnique({ where: { id }, include: withPlan });
  const moduleKeys = Array.from(new Set([...CORE_MODULE_KEYS, ...body.moduleKeys]));
  const afterPlan = body.planId === before?.planId ? before.plan : await prisma.plan.findUnique({ where: { id: body.planId } });

  const organization = await prisma.organization.update({
    where: { id },
    data: {
      planId: body.planId,
      moduleKeys,
      maxUsers: body.limits.maxUsers,
      maxProjects: body.limits.maxProjects,
      storageGb: body.limits.storageGb,
      priceMonthly: body.priceMonthly,
    },
    include: withPlan,
  });

  const changes: AuditChange[] = [];
  if (before && before.planId !== body.planId) {
    changes.push({ field: 'plan', before: before.plan?.name ?? 'Custom', after: afterPlan?.name ?? 'Custom' });
  }
  if (before && before.maxUsers !== body.limits.maxUsers) {
    changes.push({ field: 'maxUsers', before: String(before.maxUsers), after: String(body.limits.maxUsers) });
  }
  if (before && before.maxProjects !== body.limits.maxProjects) {
    changes.push({ field: 'maxProjects', before: String(before.maxProjects), after: String(body.limits.maxProjects) });
  }
  if (before && before.storageGb !== body.limits.storageGb) {
    changes.push({ field: 'storageGb', before: String(before.storageGb), after: String(body.limits.storageGb) });
  }
  if (before && before.priceMonthly !== body.priceMonthly) {
    changes.push({ field: 'priceMonthly', before: String(before.priceMonthly), after: String(body.priceMonthly) });
  }
  const beforeModuleKeys = before?.moduleKeys ?? [];
  if (beforeModuleKeys.join(',') !== moduleKeys.join(',')) {
    changes.push({ field: 'modules', before: beforeModuleKeys.join(', '), after: moduleKeys.join(', ') });
  }

  await logAudit({
    target: { type: 'organization', id: String(id), label: organization.name },
    actorId, action: 'subscription.updated', reason: body.reason, changes,
  });
  return toApiShape(organization, await computeUsage(id));
}

export async function changeTenantAdmin(id: number, body: ChangeTenantAdminBody, actorId: number | undefined) {
  const previous = await getTenantAdminSnapshot(id);
  const previousLabel = previous ? `${previous.name} <${previous.email}>` : 'none';
  const org = await prisma.organization.findUnique({ where: { id }, select: { name: true } });

  if (body.mode === 'existing') {
    const roleId = await getOrganizationAdminRoleId();
    const user = await userModel.updateUserOrganization(body.userId, id);
    if (roleId) await userModel.updateUserRole(body.userId, roleId);
    await logAudit({
      target: { type: 'organization', id: String(id), label: org?.name ?? '' },
      actorId, action: 'tenant_admin.changed',
      changes: [{ field: 'tenantAdmin', before: previousLabel, after: `${user.name} <${user.email}>` }],
    });
    return { mode: 'existing' as const, userId: body.userId };
  }

  await inviteModel.revokePendingInvites(id);
  const invite = await inviteModel.createInvite(id, { name: body.name, email: body.email }, actorId);
  await inviteModel.sendOrganizationAdminInviteEmail(invite, org?.name ?? '');
  await logAudit({
    target: { type: 'organization', id: String(id), label: org?.name ?? '' },
    actorId, action: 'tenant_admin.changed',
    changes: [{ field: 'tenantAdmin', before: previousLabel, after: `${body.name} <${body.email}>` }],
  });
  return { mode: 'invite' as const, invite };
}

export async function resendTenantAdminInvite(id: number, actorId: number | undefined) {
  const latest = await inviteModel.getLatestInvite(id);
  if (!latest) return null;
  if (latest.status === 'accepted') return 'already-accepted' as const;

  await inviteModel.revokePendingInvites(id);
  const invite = await inviteModel.createInvite(id, { name: latest.name, email: latest.email }, actorId);
  const org = await prisma.organization.findUnique({ where: { id }, select: { name: true } });
  await inviteModel.sendOrganizationAdminInviteEmail(invite, org?.name ?? '');
  await logAudit({
    target: { type: 'organization', id: String(id), label: org?.name ?? '' },
    actorId, action: 'tenant_admin.invite_resent',
    changes: [{ field: 'invite', before: 'expired or unseen', after: invite.email }],
  });
  return invite;
}
