import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { sql } from '../db.js';
import type { StaffTag, User, UserRole } from '@wa-ticketing/shared';
import { STAFF_TAGS } from '@wa-ticketing/shared';
import { auditService } from './auditService.js';

const JWT_SECRET  = process.env.JWT_SECRET!;
const JWT_EXPIRY  = process.env.JWT_EXPIRY ?? '8h';
const SALT_ROUNDS = 12;

function normalizeStaffTags(raw: unknown): StaffTag[] {
  if (!Array.isArray(raw)) return [];
  const allowed = new Set<string>(STAFF_TAGS);
  const out: StaffTag[] = [];
  for (const t of raw) {
    const v = String(t).toLowerCase().trim();
    if (allowed.has(v) && !out.includes(v as StaffTag)) out.push(v as StaffTag);
  }
  return out;
}

/** E.164-ish for staff WhatsApp; empty → null */
function normalizeStaffPhone(raw: string | null | undefined): string | null {
  const t = raw?.trim() ?? '';
  if (!t) return null;
  const digits = t.replace(/[^\d+]/g, '');
  if (digits.replace(/\D/g, '').length < 8) {
    throw Object.assign(new Error('Phone number looks invalid'), { statusCode: 400 });
  }
  return digits.startsWith('+') ? digits : `+${digits}`;
}

function mapUser(row: User): User {
  return {
    ...row,
    staff_tags: normalizeStaffTags(row.staff_tags),
  };
}

export const authService = {
  async login(email: string, password: string): Promise<{ token: string; user: User }> {
    const [row] = await sql<[User & { password_hash: string }]>`
      SELECT u.id, u.email, u.display_name, u.phone, u.password_hash, u.role, u.company_id,
             u.ai_agent_id, u.is_active, u.created_at, u.last_login_at,
             COALESCE(u.staff_tags, '{}') AS staff_tags, co.name AS company_name
      FROM users u
      LEFT JOIN companies co ON co.id = u.company_id
      WHERE u.email = ${email.toLowerCase()} AND u.is_active = TRUE
    `;

    if (!row) {
      await auditService.log(null, 'system', 'user.login_failed', 'user', '00000000-0000-0000-0000-000000000000', { email });
      throw Object.assign(new Error('Invalid credentials'), { statusCode: 401 });
    }

    const valid = await bcrypt.compare(password, row.password_hash);
    if (!valid) {
      await auditService.log(null, 'system', 'user.login_failed', 'user', row.id, { email });
      throw Object.assign(new Error('Invalid credentials'), { statusCode: 401 });
    }

    await sql`UPDATE users SET last_login_at = NOW() WHERE id = ${row.id}`;
    await auditService.log(row.id, 'user', 'user.login', 'user', row.id, {});

    const token = jwt.sign(
      {
        sub: row.id,
        email: row.email,
        role: row.role,
        company_id: row.company_id ?? null,
      },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRY } as jwt.SignOptions,
    );

    const { password_hash: _, ...user } = row;
    return { token, user: mapUser(user) };
  },

  /** Staff accounts (admin / agent) — never tied to a company */
  async createUser(
    email: string,
    password: string,
    role: 'admin' | 'user',
    aiAgentId?: string | null,
    opts?: { phone?: string | null; staffTags?: StaffTag[] },
  ): Promise<User> {
    const hash = await bcrypt.hash(password, SALT_ROUNDS);
    const aid = aiAgentId?.trim() || null;
    const phone = normalizeStaffPhone(opts?.phone ?? null);
    const tags = normalizeStaffTags(opts?.staffTags ?? []);
    const [user] = await sql<[User]>`
      INSERT INTO users (email, password_hash, role, phone, ai_agent_id, staff_tags)
      VALUES (${email.toLowerCase()}, ${hash}, ${role}, ${phone}, ${aid}, ${tags})
      RETURNING id, email, display_name, phone, role, company_id, ai_agent_id, is_active,
                created_at, last_login_at, COALESCE(staff_tags, '{}') AS staff_tags
    `;
    return mapUser(user);
  },

  async createCompanyPortalUser(
    email: string,
    password: string,
    companyId: string,
    displayName?: string | null,
    phone?: string | null,
  ): Promise<User> {
    const hash = await bcrypt.hash(password, SALT_ROUNDS);
    const dn = displayName?.trim() ? displayName.trim() : null;
    const ph = phone?.trim() ? phone.trim() : null;
    const [user] = await sql<[User]>`
      INSERT INTO users (email, password_hash, role, company_id, display_name, phone, staff_tags)
      VALUES (${email.toLowerCase()}, ${hash}, 'company_user', ${companyId}, ${dn}, ${ph}, '{}')
      RETURNING id, email, display_name, phone, role, company_id, ai_agent_id, is_active,
                created_at, last_login_at, COALESCE(staff_tags, '{}') AS staff_tags
    `;
    return mapUser(user);
  },

  async getUser(id: string): Promise<User | null> {
    const [row] = await sql<[User]>`
      SELECT u.id, u.email, u.display_name, u.phone, u.role, u.company_id, u.ai_agent_id,
             u.is_active, u.created_at, u.last_login_at,
             COALESCE(u.staff_tags, '{}') AS staff_tags, co.name AS company_name
      FROM users u
      LEFT JOIN companies co ON co.id = u.company_id
      WHERE u.id = ${id}
    `;
    return row ? mapUser(row) : null;
  },

  async listUsers(): Promise<User[]> {
    const rows = await sql<User[]>`
      SELECT id, email, display_name, phone, role, company_id, ai_agent_id, is_active,
             created_at, last_login_at, COALESCE(staff_tags, '{}') AS staff_tags
      FROM users ORDER BY created_at DESC
    `;
    return rows.map(mapUser);
  },

  /** Active admin/agent accounts for the Manage → Staff directory. */
  async listStaffDirectory(): Promise<User[]> {
    const rows = await sql<User[]>`
      SELECT id, email, display_name, phone, role, company_id, ai_agent_id, is_active,
             created_at, last_login_at, COALESCE(staff_tags, '{}') AS staff_tags
      FROM users
      WHERE is_active = TRUE AND role IN ('admin', 'user')
      ORDER BY COALESCE(NULLIF(display_name, ''), email)
    `;
    return rows.map(mapUser);
  },

  isStaffRole(user: User | null): boolean {
    return Boolean(user && (user.role === 'admin' || user.role === 'user'));
  },

  async updateUser(
    id: string,
    patch: {
      role?: UserRole;
      is_active?: boolean;
      ai_agent_id?: string | null;
      phone?: string | null;
      staff_tags?: StaffTag[];
    },
  ): Promise<User> {
    if (
      patch.role === undefined &&
      patch.is_active === undefined &&
      patch.ai_agent_id === undefined &&
      patch.phone === undefined &&
      patch.staff_tags === undefined
    ) {
      throw new Error('Nothing to update');
    }
    const cur = await this.getUser(id);
    if (!cur) throw Object.assign(new Error('User not found'), { statusCode: 404 });
    const role = patch.role ?? cur.role;
    const is_active = patch.is_active ?? cur.is_active;
    const ai_agent_id = patch.ai_agent_id !== undefined ? patch.ai_agent_id : cur.ai_agent_id;
    const phone =
      patch.phone !== undefined ? normalizeStaffPhone(patch.phone) : cur.phone;
    const staff_tags =
      patch.staff_tags !== undefined ? normalizeStaffTags(patch.staff_tags) : (cur.staff_tags ?? []);
    const tagsToStore = role === 'company_user' ? [] : staff_tags;

    const [user] = await sql<[User]>`
      UPDATE users SET
        role = ${role},
        is_active = ${is_active},
        ai_agent_id = ${ai_agent_id},
        phone = ${phone},
        staff_tags = ${tagsToStore}
      WHERE id = ${id}
      RETURNING id, email, display_name, phone, role, company_id, ai_agent_id, is_active,
                created_at, last_login_at, COALESCE(staff_tags, '{}') AS staff_tags
    `;
    return mapUser(user);
  },

  async deactivateUser(id: string): Promise<void> {
    await sql`UPDATE users SET is_active = FALSE WHERE id = ${id}`;
  },

  async changePassword(id: string, newPassword: string): Promise<void> {
    const hash = await bcrypt.hash(newPassword, SALT_ROUNDS);
    await sql`UPDATE users SET password_hash = ${hash} WHERE id = ${id}`;
  },
};
