import { Bug, BugStatus, Prisma } from '@prisma/client';
import { prisma } from '../db';
import { badRequest, notFound } from '../lib/errors';
import { idOrCodeWhere } from '../lib/params';
import { AuthUser, assertCan, loadProjectContext } from '../lib/rbac';
import { statusFromBugDb, statusToBugDb } from '../serializers/bug';

type BugFull = Bug & {
  attachments: { id: string; kind: string | null }[];
  assignments: { employeeId: string }[];
  parentTask: {
    id: string;
    projectId: string;
    project: { adminId: string; requireBugRetestEvidence: boolean };
  };
};

async function loadBug(bugId: string): Promise<BugFull> {
  const bug = await prisma.bug.findFirst({
    where: idOrCodeWhere(bugId),
    include: {
      attachments: { select: { id: true, kind: true } },
      assignments: { select: { employeeId: true } },
      parentTask: {
        select: {
          id: true,
          projectId: true,
          project: { select: { adminId: true, requireBugRetestEvidence: true } },
        },
      },
    },
  });
  if (!bug) throw notFound('Bug not found');
  return bug;
}

export function hasUnitTestEvidence(bug: {
  unitTestEvidence: Prisma.JsonValue | null;
  attachments?: { kind: string | null }[];
}): boolean {
  const unitAtt = (bug.attachments || []).filter((a) => a.kind === 'unit-test');
  if (unitAtt.length) return true;
  const ev = bug.unitTestEvidence as {
    type?: string;
    attachmentId?: string;
    note?: string;
  } | null;
  if (!ev) return false;
  if (ev.type === 'file' && ev.attachmentId) return true;
  if (
    ev.type === 'typed' &&
    String(ev.note || '').trim() &&
    !/^done\.?$/i.test(String(ev.note).trim())
  ) {
    return true;
  }
  return false;
}

export function validateReadyForRetest(bug: {
  unitTestEvidence: Prisma.JsonValue | null;
  attachments?: { kind: string | null }[];
}): string[] {
  const errors: string[] = [];
  if (!hasUnitTestEvidence(bug)) {
    errors.push(
      'Unit-test evidence is required before Ready for Retest. Upload it on the Attachments tab.'
    );
  }
  return errors;
}

async function logBugActivity(
  userId: string,
  bugId: string,
  action: string,
  details: string
) {
  await prisma.activityLog.create({
    data: {
      entityType: 'bug',
      entityId: bugId,
      userId,
      action,
      details: { text: details },
    },
  });
}

export async function tryBugTransition(
  user: AuthUser,
  bugId: string,
  toStatusLabel: string,
  details?: string
) {
  const bug = await loadBug(bugId);
  const fromLabel = statusFromBugDb(bug.status);
  const toStatus = statusToBugDb(toStatusLabel);
  const toLabel = statusFromBugDb(toStatus);

  if (bug.status === toStatus) {
    return { bug, message: 'Already at ' + toLabel };
  }

  const pctx = await loadProjectContext(user.id, bug.parentTask.projectId);
  const assignIds = (bug.assignments || []).map((a) => a.employeeId);
  assertCan(user, 'change_bug_status', {
    ...pctx,
    isBugAssignee: bug.assigneeId === user.id || assignIds.includes(user.id),
    bugStatus: fromLabel,
    toStatus: toLabel,
  });

  if (
    toStatus === BugStatus.ReadyForRetest &&
    bug.parentTask.project.requireBugRetestEvidence !== false
  ) {
    const errors = validateReadyForRetest(bug);
    if (errors.length) {
      await logBugActivity(
        user.id,
        bug.id,
        'transition_blocked',
        errors.join(' ')
      );
      throw badRequest('Transition blocked', { errors });
    }
  }

  let resolutionNotes = bug.resolutionNotes || '';
  if (toStatus === BugStatus.Closed) {
    const note = String(details || '').trim() || 'Closed';
    const stamp = new Date().toISOString().slice(0, 10);
    resolutionNotes = resolutionNotes
      ? resolutionNotes + '\n[' + stamp + '] ' + note
      : '[' + stamp + '] ' + note;
  }

  const updated = await prisma.bug.update({
    where: { id: bug.id },
    data: {
      status: toStatus,
      resolutionNotes,
    },
  });

  await logBugActivity(
    user.id,
    bug.id,
    'status_change',
    fromLabel + ' → ' + toLabel + (details ? ' — ' + details : '')
  );

  return { bug: updated, message: fromLabel + ' → ' + toLabel };
}

/** When assignee is set and status is Open, move to Assigned. */
export function nextStatusOnAssignee(
  current: BugStatus,
  assigneeId: string | null | undefined
): BugStatus {
  if (assigneeId && current === BugStatus.Open) return BugStatus.Assigned;
  return current;
}
