import { randomUUID } from 'crypto';
import { sql } from '../db.js';
import type { Channel, ChannelKind, ChannelSettings, WhatsAppProvider } from '@wa-ticketing/shared';
import { mergeChannelSettings, validateChannelSettings, PORTAL_CHANNEL_PHONE, isWebChatSystemChannel } from '@wa-ticketing/shared';
import { auditService } from './auditService.js';
import { getWhatsAppCapabilities } from './whatsappCapabilities.js';
import { planChannelCreate } from './channelProviderRules.js';
import { requireWhatsAppProviderMethod, resolveWhatsAppProvider } from './whatsappProvider.js';
import type { WhatsAppMediaPayload, WhatsAppProviderAdapter } from './whatsappProvider.js';
import { trackOutboundProviderStatus } from './providerStatusLifecycle.js';
import type { Server as SocketServer } from 'socket.io';

type ChannelRow = Channel & { settings?: unknown };

/** Guards the `::uuid` cast so a malformed public token is a miss, not a database error. */
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

function mapChannelRow(row: ChannelRow): Channel {
  const { settings: raw, ...rest } = row;
  const capabilities = rest.provider ? getWhatsAppCapabilities(rest.provider) : undefined;
  return { ...rest, ...(capabilities && { capabilities }), settings: mergeChannelSettings(raw) };
}

/** Oldest WhatsApp channel — web chat inherits settings from it only. */
async function fetchTemplateChannelRow(): Promise<ChannelRow | null> {
  const [row] = await sql<[ChannelRow]>`
    SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
    FROM channels
    WHERE kind = 'whatsapp'
    ORDER BY created_at ASC
    LIMIT 1
  `;
  return row ?? null;
}

function applyWebChatInheritedSettings(portal: Channel, template: Channel | null): Channel {
  return {
    ...portal,
    settings: template?.settings ?? mergeChannelSettings(null),
    web_chat_settings_source: template ? { id: template.id, name: template.name } : null,
  };
}

function stripInboundTokenForList(ch: Channel): Channel {
  if (ch.kind !== 'email') return { ...ch, inbound_token: undefined };
  return { ...ch, inbound_token: null };
}

export const channelService = {
  async list(): Promise<Channel[]> {
    const templateRow = await fetchTemplateChannelRow();
    const templateCh = templateRow ? mapChannelRow(templateRow) : null;

    const rows = await sql<Array<ChannelRow>>`
      SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      FROM channels
      ORDER BY (phone_number = ${PORTAL_CHANNEL_PHONE}) DESC, created_at DESC
    `;

    return rows.map(r => {
      const ch = stripInboundTokenForList(mapChannelRow(r));
      return isWebChatSystemChannel(ch.phone_number) ? applyWebChatInheritedSettings(ch, templateCh) : ch;
    });
  },

  async getById(id: string): Promise<Channel | null> {
    const [row] = await sql<[ChannelRow]>`
      SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      FROM channels WHERE id = ${id}
    `;
    if (!row) return null;
    const ch = mapChannelRow(row);
    if (!isWebChatSystemChannel(ch.phone_number)) return ch;

    const templateRow = await fetchTemplateChannelRow();
    const templateCh = templateRow ? mapChannelRow(templateRow) : null;
    return applyWebChatInheritedSettings(ch, templateCh);
  },

  /** Email webhook only — a WhatsApp provider token must never resolve here. */
  async getByInboundToken(token: string): Promise<Channel | null> {
    const [row] = await sql<[ChannelRow]>`
      SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      FROM channels
      WHERE inbound_token = ${token}::uuid AND kind = 'email'
    `;
    return row ? mapChannelRow(row) : null;
  },

  /** Public WhatsApp webhook: the token must belong to a channel on that exact provider. */
  async getWhatsAppByInboundToken(token: string, provider: WhatsAppProvider): Promise<Channel | null> {
    if (!UUID_RE.test(token)) return null;
    const [row] = await sql<[ChannelRow]>`
      SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      FROM channels
      WHERE inbound_token = ${token}::uuid AND kind = 'whatsapp' AND provider = ${provider}
    `;
    return row ? mapChannelRow(row) : null;
  },

  async create(name: string, phoneNumber: string, provider: WhatsAppProvider = 'baileys'): Promise<Channel> {
    if (isWebChatSystemChannel(phoneNumber)) {
      throw Object.assign(new Error('Cannot create a second web chat channel'), { statusCode: 400 });
    }
    // Validates provider secrets before any insert; Africa's Talking is connected on creation.
    const plan = planChannelCreate(provider);
    const [ch] = await sql<[ChannelRow]>`
      INSERT INTO channels (name, phone_number, status, kind, provider, inbound_token, last_connected_at)
      VALUES (
        ${name}, ${phoneNumber}, ${plan.status}, 'whatsapp', ${plan.provider},
        ${plan.inboundToken}::uuid, ${plan.status === 'connected' ? sql`NOW()` : null}
      )
      RETURNING id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
    `;
    await auditService.log(null, 'system', 'channel.added', 'channel', ch.id, {
      name, phoneNumber, kind: 'whatsapp', provider: plan.provider,
    });
    return mapChannelRow(ch);
  },

  async createEmailChannel(name: string): Promise<Channel> {
    const token = randomUUID();
    const [ch] = await sql<[ChannelRow]>`
      INSERT INTO channels (name, phone_number, status, kind, inbound_token, last_connected_at)
      VALUES (${name.trim()}, 'email:pending', 'connected', 'email', ${token}::uuid, NOW())
      RETURNING id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
    `;
    const synthetic = `email:${ch.id}`;
    await sql`UPDATE channels SET phone_number = ${synthetic} WHERE id = ${ch.id}`;
    await auditService.log(null, 'system', 'channel.added', 'channel', ch.id, { name, kind: 'email' });
    return (await this.getById(ch.id))!;
  },

  async update(id: string, patch: { name?: string; settings?: ChannelSettings }, actorId: string): Promise<Channel | null> {
    const [meta] = await sql<[{ phone_number: string; kind: ChannelKind }]>`
      SELECT phone_number, kind FROM channels WHERE id = ${id}
    `;
    if (!meta) return null;

    const [existingRow] = await sql<[ChannelRow]>`
      SELECT id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      FROM channels WHERE id = ${id}
    `;
    if (!existingRow) return null;

    const name = patch.name?.trim() ?? existingRow.name;

    if (meta.kind === 'portal' || isWebChatSystemChannel(meta.phone_number)) {
      const [row] = await sql<[ChannelRow]>`
        UPDATE channels SET name = ${name} WHERE id = ${id}
        RETURNING id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
      `;
      if (!row) return null;
      await auditService.log(actorId, 'user', 'channel.updated', 'channel', id, { name: patch.name !== undefined });
      return this.getById(id);
    }

    const settings = patch.settings ?? mergeChannelSettings(existingRow.settings);
    if (patch.settings !== undefined) validateChannelSettings(settings);
    const [row] = await sql<[ChannelRow]>`
      UPDATE channels
      SET name = ${name}, settings = ${sql.json(settings as never)}
      WHERE id = ${id}
      RETURNING id, name, phone_number, kind, provider, status, created_at, last_connected_at, settings, inbound_token
    `;
    if (!row) return null;
    await auditService.log(actorId, 'user', 'channel.updated', 'channel', id, {
      name: patch.name !== undefined,
      settings: patch.settings !== undefined,
    });
    return mapChannelRow(row);
  },

  async remove(id: string, actorId: string): Promise<void> {
    const [meta] = await sql<[{ phone_number: string; kind: ChannelKind }]>`
      SELECT phone_number, kind FROM channels WHERE id = ${id}
    `;
    if (!meta) return;
    if (isWebChatSystemChannel(meta.phone_number) || meta.kind === 'portal') {
      throw Object.assign(new Error('The web chat channel cannot be removed'), { statusCode: 400 });
    }
    if (meta.kind === 'whatsapp') {
      await this.terminateSession(id, 'whatsapp').catch(() => {});
    }
    await sql`DELETE FROM channels WHERE id = ${id}`;
    await auditService.log(actorId, 'user', 'channel.removed', 'channel', id, {});
  },

  async setStatus(id: string, status: Channel['status']): Promise<void> {
    await sql`
      UPDATE channels
      SET status = ${status},
          last_connected_at = CASE WHEN ${status} = 'connected' THEN NOW() ELSE last_connected_at END
      WHERE id = ${id}
    `;
  },

  async saveSession(id: string, sessionData: unknown): Promise<void> {
    await sql`
      UPDATE channels SET session_data = ${sql.json(sessionData as never)}
      WHERE id = ${id}
    `;
  },

  async loadSession(id: string): Promise<unknown | null> {
    const [row] = await sql<[{ session_data: unknown }]>`
      SELECT session_data FROM channels WHERE id = ${id}
    `;
    return row?.session_data ?? null;
  },

  /** Resolves the persisted channel and its provider adapter (404 if missing, 400 if not WhatsApp). */
  async resolveAdapter(channelId: string): Promise<{ channel: Channel; adapter: WhatsAppProviderAdapter }> {
    const channel = await this.getById(channelId);
    if (!channel) throw Object.assign(new Error('Channel not found'), { statusCode: 404 });
    return { channel, adapter: resolveWhatsAppProvider(channel) };
  },

  async terminateSession(id: string, channelKind: ChannelKind = 'whatsapp'): Promise<void> {
    if (channelKind === 'whatsapp') await this.clearWorkerSession(id);
    await sql`
      UPDATE channels SET status = 'disconnected', session_data = NULL WHERE id = ${id}
    `;
  },

  /** Best-effort worker cleanup; providers without sessions (or a worker that is down) are a no-op. */
  async clearWorkerSession(id: string): Promise<void> {
    try {
      const { channel, adapter } = await this.resolveAdapter(id);
      await adapter.terminateSession?.(channel);
    } catch {
      /* Worker down, no session, or provider without sessions */
    }
  },

  /**
   * Start (or force re-pair) a Baileys session.
   * When clearAuth is true the worker wipes stored WhatsApp creds so a new QR is emitted.
   */
  async startSession(channelId: string, _io: SocketServer, opts?: { clearAuth?: boolean }): Promise<void> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    await requireWhatsAppProviderMethod(channel, adapter, 'startSession')(channel, opts?.clearAuth === true);
  },

  async restoreAllSessions(io: SocketServer): Promise<void> {
    const channels = await sql<Array<{ id: string; name: string; phone_number: string; status: Channel['status'] }>>`
      SELECT id, name, phone_number, status
      FROM channels
      WHERE kind = 'whatsapp'
        AND provider = 'baileys'
        AND last_connected_at IS NOT NULL
    `;

    for (const ch of channels) {
      try {
        await this.startSession(ch.id, io);
      } catch (err) {
        const msg = String(err);
        if (msg.includes('fetch failed') || msg.includes('ECONNREFUSED')) {
          console.warn(`[channelService] Worker not ready for ${ch.id}, skipping`);
        } else {
          console.error(`[channelService] Failed to restore session for ${ch.id}:`, err);
          await this.setStatus(ch.id, 'disconnected');
          io.emit('channel:disconnected', { channelId: ch.id, reason: 'Session invalid on restart' });
          await auditService.log(null, 'system', 'channel.disconnected', 'channel', ch.id, {
            reason: 'Session invalid on restart',
          });
        }
      }
    }
  },

  async sendMessage(channelId: string, phoneOrJid: string, text: string): Promise<string | null> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    return trackOutboundProviderStatus(channel, (id, status) => this.setStatus(id, status), () =>
      adapter.sendText(channel, phoneOrJid, text),
    );
  },

  /** Create a WhatsApp group; returns group JID (`…@g.us`). */
  async createWhatsAppGroup(
    channelId: string,
    subject: string,
    phones: string[],
  ): Promise<{ groupJid: string; subject: string }> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    return requireWhatsAppProviderMethod(channel, adapter, 'createGroup')(channel, subject, phones);
  },

  /**
   * Outbound image, voice note or document. `mediaUrl` is a hosted (signed) URL for providers that
   * cannot take base64; callers that only have bytes may omit it (Baileys does not need it).
   */
  async sendMediaMessage(
    channelId: string,
    phoneOrJid: string,
    payload: Omit<WhatsAppMediaPayload, 'mediaUrl'> & { mediaUrl?: string | null },
  ): Promise<string | null> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    return trackOutboundProviderStatus(channel, (id, status) => this.setStatus(id, status), () =>
      adapter.sendMedia(channel, phoneOrJid, { ...payload, mediaUrl: payload.mediaUrl ?? null }),
    );
  },

  async getGroupMetadata(
    channelId: string,
    groupJid: string,
  ): Promise<{ groupJid: string; subject: string; phones: string[]; participantCount: number }> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    return requireWhatsAppProviderMethod(channel, adapter, 'getGroupMetadata')(channel, groupJid);
  },

  async updateGroupParticipants(
    channelId: string,
    groupJid: string,
    action: 'add' | 'remove',
    phones: string[],
  ): Promise<void> {
    const { channel, adapter } = await this.resolveAdapter(channelId);
    await requireWhatsAppProviderMethod(channel, adapter, 'updateGroupParticipants')(channel, groupJid, action, phones);
  },

};
