import {
  Prisma,
  QaResult,
  Task,
  TaskStage,
} from '@prisma/client';
import { prisma } from '../db';
import { DeliveryRuleKey, ruleEnabled } from '../lib/deliveryRules';
import { badRequest, forbidden, notFound } from '../lib/errors';
import { AuthUser, assertCan, loadProjectContext } from '../lib/rbac';
import { stageFromDb, stageIndex, stageToDb, STAGE_LABELS } from '../lib/stages';

type ProjectPolicy = {
  id: string;
  adminId: string;
  internalStatuses: { id: string; text: string; stage: TaskStage; active: boolean }[];
} & Partial<Record<DeliveryRuleKey, boolean>>;

type TaskFull = Task & {
  assignments: { employeeId: string; allocatedHours: Prisma.Decimal }[];
  attachments: { kind: string | null }[];
  project: ProjectPolicy;
};

async function loadTask(taskId: string): Promise<TaskFull> {
  const task = await prisma.task.findFirst({
    where: { OR: [{ id: taskId }, { code: taskId }] },
    include: {
      assignments: true,
      attachments: { select: { kind: true } },
      project: {
        include: {
          internalStatuses: true,
        },
      },
    },
  });
  if (!task) throw notFound('Task not found');
  return task;
}

function statusText(task: TaskFull): string {
  const st = task.project.internalStatuses.find((x) => x.id === task.internalStatusId);
  return st ? st.text : '';
}

function policy(task: TaskFull, key: DeliveryRuleKey): boolean {
  return ruleEnabled(task.project, key);
}

async function requiredSubtasksComplete(taskId: string) {
  const subs = await prisma.task.findMany({
    where: { parentId: taskId, required: true },
    select: { id: true, code: true, title: true, stage: true },
  });
  if (!subs.length) return { ok: true, missing: [] as string[] };
  const incomplete = subs.filter((t) => t.stage !== TaskStage.Done);
  return {
    ok: incomplete.length === 0,
    missing: incomplete.map((t) => `${t.code} — ${t.title}`),
  };
}

async function blockingDeps(task: Task) {
  const deps = task.dependencies || [];
  if (!deps.length) return [];
  const open = await prisma.task.findMany({
    where: { id: { in: deps }, stage: { not: TaskStage.Done } },
    select: { code: true },
  });
  return open;
}

async function blockingBugs(taskId: string) {
  return prisma.bug.findMany({
    where: {
      parentTaskId: taskId,
      blocking: true,
      status: { not: 'Closed' },
    },
    select: { code: true },
  });
}

export async function validateGateA(task: TaskFull): Promise<string[]> {
  const errors: string[] = [];
  const assignments = task.assignments || [];

  if (policy(task, 'requireAssigneeHours')) {
    if (!task.assigneeId && !assignments.length) {
      errors.push('At least one assignee is required.');
    }
    const totalAlloc = assignments.length
      ? assignments.reduce((a, x) => a + Number(x.allocatedHours || 0), 0)
      : Number(task.allocatedHours || 0);
    if (!(totalAlloc > 0)) {
      errors.push('Allocated hours must be greater than zero for assigned members.');
    }
    if (assignments.length && assignments.some((a) => !(Number(a.allocatedHours) > 0))) {
      errors.push('Every assigned member must have allocated hours greater than zero.');
    }
  }

  if (policy(task, 'requireBaAnalysis') && !task.baAnalysisComplete) {
    errors.push('BA / requirement analysis must be marked complete.');
  }

  if (policy(task, 'requireDescriptionAc')) {
    const stripHtml = (s: string) =>
      String(s || '')
        .replace(/<[^>]+>/g, ' ')
        .replace(/&nbsp;/g, ' ')
        .trim();
    const descText = stripHtml(task.description || '');
    if (!descText) {
      errors.push('Requirement description is required.');
    }
    const acText = stripHtml(task.acceptanceCriteria || '');
    if (!acText) {
      errors.push('Acceptance criteria are required.');
    }
  }

  if (
    policy(task, 'requireRequirementDoc') &&
    !task.attachments.some((a) => a.kind === 'requirement')
  ) {
    errors.push('A requirement document attachment is required by project policy.');
  }

  if (policy(task, 'enforceRequiredSubtasks')) {
    const sub = await requiredSubtasksComplete(task.id);
    if (!sub.ok) errors.push('Required subtasks incomplete: ' + sub.missing.join('; '));
  }

  if (policy(task, 'requireReadyForDevelopment')) {
    const st = statusText(task);
    if (st !== 'Ready for Development') {
      errors.push(
        'Internal status must be Ready for Development (currently: ' + (st || 'none') + ').'
      );
    }
  }
  return errors;
}

export async function validateGateB(task: TaskFull): Promise<string[]> {
  const errors: string[] = [];
  if (policy(task, 'requireDevelopmentComplete') && !task.developmentComplete) {
    errors.push('Development must be marked complete.');
  }

  const ev = task.unitTestEvidence as { type?: string; note?: string } | null;
  if (policy(task, 'requireUnitTestEvidence')) {
    if (!ev) {
      errors.push(
        'Unit-test evidence is required (file record or typed evidence per project policy).'
      );
    } else if (ev.type === 'typed' && !policy(task, 'allowTypedEvidence')) {
      errors.push(
        'Typed evidence is not allowed for this project — attach a file or test result record.'
      );
    } else if (ev.type === 'typed' && /^done\.?$/i.test(String(ev.note || '').trim())) {
      errors.push('Generic "done" is not valid unit-test evidence.');
    }
  } else if (ev?.type === 'typed' && !policy(task, 'allowTypedEvidence')) {
    errors.push(
      'Typed evidence is not allowed for this project — attach a file or test result record.'
    );
  }

  if (policy(task, 'enforceRequiredSubtasks')) {
    const sub = await requiredSubtasksComplete(task.id);
    if (!sub.ok) errors.push('Required subtasks incomplete: ' + sub.missing.join('; '));
  }

  if (policy(task, 'enforceDependencies')) {
    const deps = await blockingDeps(task);
    if (deps.length) {
      errors.push('Blocking dependencies remain: ' + deps.map((d) => d.code).join(', '));
    }
  }
  return errors;
}

export async function validateGateC(task: TaskFull): Promise<string[]> {
  const errors: string[] = [];
  if (policy(task, 'requireCodeReview') && !task.codeReviewApproved) {
    errors.push('Code review must be approved.');
  }
  if (policy(task, 'requireQaPassed') && task.qaResult !== QaResult.Passed) {
    errors.push(
      'QA result must be Passed (current: ' + (task.qaResult || 'none') + ').'
    );
  }
  if (policy(task, 'requireQaEvidence') && !task.qaEvidence) {
    errors.push('QA test evidence/result must be recorded.');
  }
  if (policy(task, 'blockDoneOnBlockingBugs')) {
    const bugs = await blockingBugs(task.id);
    if (bugs.length) {
      errors.push('Blocking bugs still open: ' + bugs.map((b) => b.code).join(', '));
    }
  }
  if (policy(task, 'enforceRequiredSubtasks')) {
    const sub = await requiredSubtasksComplete(task.id);
    if (!sub.ok) errors.push('Required subtasks incomplete: ' + sub.missing.join('; '));
  }
  if (policy(task, 'requireAcceptance') && !task.acceptanceSatisfied) {
    errors.push('Acceptance criteria must be marked satisfied.');
  }
  if (policy(task, 'requireTesterApproval') && !task.testerApproval) {
    errors.push('Authorized tester/reviewer approval is required.');
  }
  return errors;
}

export async function validateTransition(
  task: TaskFull,
  toStageLabel: string
): Promise<{ ok: boolean; errors: string[]; reverse?: boolean }> {
  const from = stageFromDb(task.stage);
  if (from === toStageLabel) return { ok: true, errors: [] };
  const fi = stageIndex(from);
  const ti = stageIndex(toStageLabel);
  if (fi < 0 || ti < 0) return { ok: false, errors: ['Unknown stage.'] };

  if (from === 'Done' && toStageLabel !== 'Done') {
    return {
      ok: false,
      errors: ['Use Reopen with a mandatory reason to leave Done.'],
    };
  }

  if (ti < fi) {
    if (from === 'Code Review & QA' && toStageLabel === 'In Progress') {
      return { ok: true, errors: [], reverse: true };
    }
    if (from === 'In Progress' && toStageLabel === 'Inbox') {
      return { ok: true, errors: [], reverse: true };
    }
    return {
      ok: false,
      errors: ['Backward transitions require an explicit return/reopen action.'],
    };
  }

  let errors: string[] = [];
  if (from === 'Inbox' && toStageLabel === 'In Progress') {
    errors = await validateGateA(task);
  } else if (from === 'In Progress' && toStageLabel === 'Code Review & QA') {
    errors = await validateGateB(task);
  } else if (from === 'Code Review & QA' && toStageLabel === 'Done') {
    errors = await validateGateC(task);
  } else if (ti > fi) {
    if (fi <= 0 && ti >= 1) errors = errors.concat(await validateGateA(task));
    if (fi <= 1 && ti >= 2) errors = errors.concat(await validateGateB(task));
    if (ti >= 3) errors = errors.concat(await validateGateC(task));
  }

  return { ok: errors.length === 0, errors };
}

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

async function alignInternalStatus(taskId: string, projectId: string, toStage: TaskStage) {
  const task = await prisma.task.findUnique({ where: { id: taskId } });
  if (!task) return;
  const statuses = await prisma.projectInternalStatus.findMany({
    where: { projectId, active: true },
  });
  const cur = statuses.find((x) => x.id === task.internalStatusId);
  if (!cur || cur.stage !== toStage) {
    const stageStatuses = statuses
      .filter((x) => x.stage === toStage)
      .sort((a, b) => a.sortOrder - b.sortOrder);
    // Prefer Completed (not Released) when entering Done
    const fallback =
      (toStage === TaskStage.Done
        ? stageStatuses.find((x) => x.text === 'Completed')
          || stageStatuses.find((x) => x.text !== 'Released')
        : null)
      || stageStatuses[0];
    if (fallback) {
      await prisma.task.update({
        where: { id: taskId },
        data: { internalStatusId: fallback.id },
      });
    }
  }
}

export async function tryTransition(
  user: AuthUser,
  taskId: string,
  toStageLabel: string,
  opts: { reason?: string } = {}
) {
  const task = await loadTask(taskId);
  const pctx = await loadProjectContext(user.id, task.projectId);
  const isAssignee =
    task.assigneeId === user.id ||
    task.assignments.some((a) => a.employeeId === user.id) ||
    (task.contributors || []).includes(user.id);

  assertCan(user, 'change_task_status', {
    ...pctx,
    isTaskAssignee: isAssignee,
  });

  if (!STAGE_LABELS.includes(toStageLabel as (typeof STAGE_LABELS)[number])) {
    throw badRequest('Unknown stage');
  }

  const result = await validateTransition(task, toStageLabel);
  if (!result.ok) {
    // Do not persist transition_blocked noise in the task chatter feed.
    throw badRequest('Transition blocked', { errors: result.errors });
  }
  if (result.reverse && !String(opts.reason || '').trim()) {
    throw badRequest('Transition blocked', {
      errors: [
        'Use Return to Inbox / Return to In Progress / Reopen with a reason — stage bar cannot move backward alone.',
      ],
    });
  }

  const toStage = stageToDb(toStageLabel);
  const from = stageFromDb(task.stage);

  await prisma.task.update({
    where: { id: task.id },
    data: {
      stage: toStage,
      ...(toStage === TaskStage.Done
        ? {
            completionDate: new Date(),
            completedById: user.id,
          }
        : {}),
    },
  });

  await alignInternalStatus(task.id, task.projectId, toStage);
  await logActivity(
    task.id,
    user.id,
    'stage_change',
    `${from} → ${toStageLabel}${opts.reason ? ` (${opts.reason})` : ''}`
  );

  return { ok: true as const, errors: [] as string[] };
}

export async function reopenTask(user: AuthUser, taskId: string, reason: string) {
  const task = await loadTask(taskId);
  if (task.stage !== TaskStage.Done) {
    throw badRequest('Only Done tasks can be reopened.');
  }
  if (!String(reason || '').trim()) {
    throw badRequest('A reopen reason is mandatory.');
  }

  const pctx = await loadProjectContext(user.id, task.projectId);
  const isAssignee =
    task.assigneeId === user.id ||
    task.assignments.some((a) => a.employeeId === user.id);
  assertCan(user, 'change_task_status', { ...pctx, isTaskAssignee: isAssignee });

  const qaStatus = task.project.internalStatuses.find(
    (x) => x.text === 'QA Testing' && x.active
  );

  await prisma.task.update({
    where: { id: task.id },
    data: {
      stage: TaskStage.CodeReviewQA,
      completionDate: null,
      completedById: null,
      acceptanceSatisfied: false,
      testerApproval: false,
      qaResult: null,
      internalStatusId: qaStatus?.id ?? task.internalStatusId,
    },
  });

  await logActivity(task.id, user.id, 'reopened', `Reopened: ${reason}`);
  return { ok: true as const };
}

export async function returnToInProgress(
  user: AuthUser,
  taskId: string,
  reason: string
) {
  if (!String(reason || '').trim()) throw badRequest('Reason is required.');
  const task = await loadTask(taskId);
  if (task.stage !== TaskStage.CodeReviewQA) {
    throw badRequest('Task must be in Code Review & QA.');
  }
  return tryTransition(user, task.id, 'In Progress', { reason });
}

/** Undo accidental Inbox → In Progress (reason required). */
export async function returnToInbox(
  user: AuthUser,
  taskId: string,
  reason: string
) {
  if (!String(reason || '').trim()) throw badRequest('Reason is required.');
  const task = await loadTask(taskId);
  if (task.stage !== TaskStage.InProgress) {
    throw badRequest('Task must be In Progress to return to Inbox.');
  }
  return tryTransition(user, task.id, 'Inbox', { reason });
}

export async function assertTaskAccess(
  user: AuthUser,
  task: { projectId: string; assigneeId: string | null; contributors: string[]; stage: TaskStage },
  action: 'view_task' | 'edit_task' | 'change_task_status',
  assignmentEmployeeIds: string[] = []
) {
  const pctx = await loadProjectContext(user.id, task.projectId);
  const isAssignee =
    task.assigneeId === user.id ||
    assignmentEmployeeIds.includes(user.id) ||
    (task.contributors || []).includes(user.id);
  assertCan(user, action, {
    ...pctx,
    isTaskAssignee: isAssignee,
    taskStageDone: task.stage === TaskStage.Done,
  });
  if (action === 'view_task' && !pctx.isProjectMember && !isAssignee && user.role !== 'SuperAdmin') {
    if (!canView(user, pctx.isProjectMember, isAssignee, pctx.projectAdminId === user.id)) {
      throw forbidden('You cannot view this task');
    }
  }
}

function canView(
  user: AuthUser,
  isMember: boolean | undefined,
  isAssignee: boolean,
  isAdmin: boolean
) {
  if (user.role === 'SuperAdmin') return true;
  return Boolean(isMember || isAssignee || isAdmin);
}
