/**
 * Admin-user orchestration (PLANNING.md §4b, §8). Controllers stay HTTP-only.
 *
 * The superadmin sets the initial password directly on create — the account is
 * usable immediately, no invite email or token step. `issueSetupToken` is still
 * used by `changePassword()`'s superadmin-force-reset path (emailing a fresh
 * set-password link to an existing account), just not on create. No hard
 * delete — `status='inactive'` is deactivation. Guard rails: you can't change
 * your own role or deactivate yourself, and the last active superadmin can't be
 * demoted or deactivated.
 */
import { AppError } from '../../lib/AppError';
import { fromSqlUtc } from '../../lib/dates';
import { normalizePhone } from '../../lib/phone';
import { issueSetupToken } from '../auth/auth.service';
import { clearLockout, updateAdminPassword } from '../auth/auth.repository';
import { hashPassword, verifyPassword } from '../auth/password.service';
import { revokeAllForUser } from '../auth/token.service';
import type {
  ChangePasswordInput,
  CreateUserInput,
  ListQuery,
  UpdateUserInput,
} from './user.schema';
import {
  countActiveSuperadmins,
  countUsers,
  emailInUse,
  findUserById,
  insertUser,
  listUsers as repoListUsers,
  phoneInUse,
  updateUser as repoUpdateUser,
  type Role,
  type UserRow,
} from './user.repository';

export interface Actor {
  id: string;
  role: Role;
}

export interface UserDto {
  id: string;
  name: string;
  phone: string;
  email: string | null;
  role: Role;
  status: 'active' | 'inactive';
  /** false => the invite hasn't been accepted, this account can't log in yet. */
  hasPassword: boolean;
  lastLoginAt: string | null;
  createdBy: string | null;
  createdAt: string;
  updatedAt: string;
}

function toDto(row: UserRow): UserDto {
  return {
    id: row.id,
    name: row.name,
    phone: row.phone_display,
    email: row.email,
    role: row.role,
    status: row.status,
    hasPassword: row.password_hash !== null,
    lastLoginAt: row.last_login_at ? fromSqlUtc(row.last_login_at).toISOString() : null,
    createdBy: row.created_by,
    createdAt: fromSqlUtc(row.created_at).toISOString(),
    updatedAt: fromSqlUtc(row.updated_at).toISOString(),
  };
}

async function requireUser(id: string): Promise<UserRow> {
  const row = await findUserById(id);
  if (!row) throw AppError.notFound('User not found');
  return row;
}

// ─── endpoints ─────────────────────────────────────────────────────────────

export async function list(query: ListQuery): Promise<{
  data: UserDto[];
  meta: { page: number; perPage: number; total: number };
}> {
  const filter = { q: query.q, status: query.status, role: query.role };
  const offset = (query.page - 1) * query.perPage;
  const [rows, total] = await Promise.all([
    repoListUsers(filter, { limit: query.perPage, offset, sort: query.sort }),
    countUsers(filter),
  ]);
  return {
    data: rows.map(toDto),
    meta: { page: query.page, perPage: query.perPage, total },
  };
}

export async function getById(id: string): Promise<UserDto> {
  return toDto(await requireUser(id));
}

export async function create(input: CreateUserInput, createdBy: string): Promise<UserDto> {
  const { e164, display } = normalizePhone(input.phone);

  if (await phoneInUse(e164)) {
    throw AppError.validation({ phone: 'That phone number is already registered' });
  }
  if (await emailInUse(input.email)) {
    throw AppError.validation({ email: 'That email is already registered' });
  }

  const id = await insertUser({
    name: input.name,
    phone: e164,
    phone_display: display,
    email: input.email,
    role: input.role,
    status: 'active',
    created_by: createdBy,
    password_hash: await hashPassword(input.password),
  });

  return getById(id);
}

export async function update(id: string, patch: UpdateUserInput, actor: Actor): Promise<UserDto> {
  const target = await requireUser(id);
  const isSelf = id === actor.id;

  if (isSelf && patch.role && patch.role !== target.role) {
    throw AppError.badRequest("You can't change your own role");
  }
  if (isSelf && patch.status === 'inactive') {
    throw AppError.badRequest("You can't deactivate your own account");
  }

  const demoting = patch.role === 'admin' && target.role === 'superadmin';
  const deactivating = patch.status === 'inactive' && target.status === 'active';
  if ((demoting || deactivating) && target.role === 'superadmin' && target.status === 'active') {
    if ((await countActiveSuperadmins(id)) === 0) {
      throw AppError.badRequest('At least one active superadmin must remain');
    }
  }

  const dbPatch: Parameters<typeof repoUpdateUser>[1] = {};
  if (patch.name !== undefined) dbPatch.name = patch.name;
  if (patch.email !== undefined) {
    if (await emailInUse(patch.email, id)) {
      throw AppError.validation({ email: 'That email is already registered' });
    }
    dbPatch.email = patch.email;
  }
  if (patch.phone !== undefined) {
    const norm = normalizePhone(patch.phone);
    if (await phoneInUse(norm.e164, id)) {
      throw AppError.validation({ phone: 'That phone number is already registered' });
    }
    dbPatch.phone = norm.e164;
    dbPatch.phone_display = norm.display;
  }
  if (patch.role !== undefined) dbPatch.role = patch.role;
  if (patch.status !== undefined) dbPatch.status = patch.status;

  await repoUpdateUser(id, dbPatch);

  // A role or status change alters what this user may do — end their sessions so
  // the new claims take effect now, not after the 15-min access token expires.
  const roleChanged = patch.role !== undefined && patch.role !== target.role;
  const statusChanged = patch.status !== undefined && patch.status !== target.status;
  if (roleChanged || statusChanged) await revokeAllForUser(id);

  return getById(id);
}

export async function changePassword(
  targetId: string,
  actor: Actor,
  body: ChangePasswordInput,
): Promise<void> {
  if (targetId === actor.id) {
    // Self-service — prove knowledge of the current password.
    if (!body.currentPassword || !body.newPassword) {
      throw AppError.badRequest('currentPassword and newPassword are required');
    }
    const me = await requireUser(targetId);
    if (!me.password_hash) {
      throw AppError.badRequest('Set your password via the invite link first');
    }
    if (!(await verifyPassword(body.currentPassword, me.password_hash))) {
      throw AppError.unauthorized('Current password is incorrect');
    }
    await updateAdminPassword(targetId, await hashPassword(body.newPassword));
    await revokeAllForUser(targetId); // sign out every other session
    await clearLockout(targetId);
    return;
  }

  // Superadmin force-reset — email a fresh set-password link, no password here.
  if (actor.role !== 'superadmin') throw AppError.forbidden();
  const target = await requireUser(targetId);
  if (!target.email) throw AppError.badRequest('User has no email on file');
  await issueSetupToken({ id: target.id, name: target.name, email: target.email }, 'reset');
}
