import { getConnection } from '../../config/database.js';
import { findAll, findById, insert, update, softDelete, findMaxCode } from '../repositories/employee.repo.js';
import { replaceForEmployee, type PermissionOverride } from '../repositories/employeePermissionOverride.repo.js';
import {
  replaceForEmployee as replaceStageTransitions, type StageTransition,
} from '../repositories/employeeStageTransition.repo.js';
import { hasOrdersForEmployee } from '../repositories/order.repo.js';
import { createWithId } from '../utils/id.js';
import { generateEmployeeCode } from '../utils/helpers.js';
import { hashPassword } from '../utils/hash.js';
import { NotFoundError, ConflictError, ValidationError } from '../utils/errors.js';
import { emitter } from '../events/emitter.js';
import { getStages } from './stage.service.js';
import type { Employee, PaginatedResult } from '../types/index.js';

// Kept for resolving a stage id to its pipeline position (used for id
// validation below) — transitions themselves are no longer required to move
// strictly forward. advanceStage()'s enforcement was always direction-
// agnostic (it just matches "does one of this employee's rows have this
// exact fromStageId/toStageId pair", regardless of which one comes first in
// the pipeline), so allowing an admin to also assign a backward move (e.g.
// wiring -> not_started, to send a line back for rework) needs no change
// there — only this admin-time restriction stood in the way.
function stageSortOrder(stageId: string): number {
  if (stageId === 'not_started') return 0;
  const index = getStages().findIndex((s) => s.id === stageId);
  if (index === -1) throw new ValidationError(`Unknown stage id "${stageId}"`);
  return index + 1;
}

function validateStageTransitions(transitions: StageTransition[]): void {
  for (const t of transitions) {
    // Still resolve both ids so an unknown/mistyped stage id is caught here
    // rather than surfacing later as a silently-never-matching rule.
    stageSortOrder(t.fromStageId);
    stageSortOrder(t.toStageId);
    if (t.fromStageId === t.toStageId) {
      throw new ValidationError(`Stage transition must go to a different stage: "${t.fromStageId}"`);
    }
  }
}

export async function list(search?: string, roleId?: string, page = 1, limit = 20): Promise<PaginatedResult<Employee>> {
  const { rows, total } = await findAll(search, roleId, page, limit);
  return { data: rows, total, page, limit };
}

export async function get(id: string): Promise<Employee> {
  const employee = await findById(id);
  if (!employee) throw new NotFoundError('Employee not found');
  return employee;
}

export async function create(
  data: Partial<Employee> & { password?: string; permission_overrides?: PermissionOverride[]; stage_transitions?: StageTransition[] },
  userId: string,
): Promise<Employee> {
  let password_hash: string | undefined;
  if (data.password) {
    password_hash = await hashPassword(data.password);
  }
  if (data.stage_transitions) validateStageTransitions(data.stage_transitions);
  if (data.pending_delivery_stage_id) stageSortOrder(data.pending_delivery_stage_id);

  const conn = await getConnection();
  try {
    await conn.beginTransaction();

    // `code` is separately unique-constrained (like `id`), so it also has to
    // be recomputed on every retry attempt — otherwise a collision on `code`
    // alone would keep failing the same way no matter how many times `id` is
    // regenerated.
    const { id } = await createWithId('employees', 'KCEMP', async (id) => {
      const lastCode = await findMaxCode(conn);
      const code = generateEmployeeCode(lastCode ?? undefined);
      return insert({ ...data, code, password_hash }, id, userId, conn);
    }, conn);

    if (data.permission_overrides) {
      await replaceForEmployee(id, data.permission_overrides, userId, conn);
    }
    if (data.stage_transitions) {
      await replaceStageTransitions(id, data.stage_transitions, userId, conn);
    }

    await conn.commit();

    const employee = await findById(id);
    if (!employee) throw new NotFoundError('Employee not found');
    emitter.emit('activity.create', { entity_type: 'employee', entity_id: id, action: 'created', user_id: userId });
    return employee;
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }
}

export async function updateEmployee(
  id: string,
  data: Partial<Employee> & { password?: string; permission_overrides?: PermissionOverride[]; stage_transitions?: StageTransition[] },
  userId: string,
): Promise<Employee> {
  const existing = await findById(id);
  if (!existing) throw new NotFoundError('Employee not found');
  if (data.stage_transitions) validateStageTransitions(data.stage_transitions);
  if (data.pending_delivery_stage_id) stageSortOrder(data.pending_delivery_stage_id);

  const updateData: Partial<Employee> & { password_hash?: string } = { ...data };
  if (data.password) {
    updateData.password_hash = await hashPassword(data.password);
  }
  delete (updateData as { password?: string }).password;
  delete (updateData as { permission_overrides?: PermissionOverride[] }).permission_overrides;
  delete (updateData as { stage_transitions?: StageTransition[] }).stage_transitions;

  const conn = await getConnection();
  try {
    await conn.beginTransaction();
    await update(id, updateData, userId, conn);
    if (data.permission_overrides) {
      await replaceForEmployee(id, data.permission_overrides, userId, conn);
    }
    if (data.stage_transitions) {
      await replaceStageTransitions(id, data.stage_transitions, userId, conn);
    }
    await conn.commit();
  } catch (err) {
    await conn.rollback();
    throw err;
  } finally {
    conn.release();
  }

  emitter.emit('activity.create', { entity_type: 'employee', entity_id: id, action: 'updated', user_id: userId });
  return get(id);
}

export async function remove(id: string, userId: string): Promise<void> {
  const existing = await findById(id);
  if (!existing) throw new NotFoundError('Employee not found');
  if (await hasOrdersForEmployee(id)) {
    throw new ConflictError('This employee has existing orders and cannot be deleted');
  }
  await softDelete(id, userId);
  emitter.emit('activity.create', { entity_type: 'employee', entity_id: id, action: 'deleted', user_id: userId });
}
