import { randomUUID } from 'crypto';
import { sql } from '../db.js';
import type { Channel, ChannelKind, ChannelSettings } from '@wa-ticketing/shared';
import { mergeChannelSettings, PORTAL_CHANNEL_PHONE, isWebChatSystemChannel } from '@wa-ticketing/shared';
import { auditService } from './auditService.js';
import type { Server as SocketServer } from 'socket.io';

const WA_WORKER_URL = process.env.WA_WORKER_URL ?? 'http://whatsapp:3002';

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

function mapChannelRow(row: ChannelRow): Channel {
  const { settings: raw, ...rest } = row;
  return { ...rest, 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, 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, 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, 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);
  },

  async getByInboundToken(token: string): Promise<Channel | null> {
    const [row] = await sql<[ChannelRow]>`
      SELECT id, name, phone_number, kind, status, created_at, last_connected_at, settings, inbound_token
      FROM channels
      WHERE inbound_token = ${token}::uuid AND kind = 'email'
    `;
    return row ? mapChannelRow(row) : null;
  },

  async create(name: string, phoneNumber: string): Promise<Channel> {
    if (isWebChatSystemChannel(phoneNumber)) {
      throw Object.assign(new Error('Cannot create a second web chat channel'), { statusCode: 400 });
    }
    const [ch] = await sql<[ChannelRow]>`
      INSERT INTO channels (name, phone_number, status, kind)
      VALUES (${name}, ${phoneNumber}, 'connecting', 'whatsapp')
      RETURNING id, name, phone_number, kind, status, created_at, last_connected_at, settings, inbound_token
    `;
    await auditService.log(null, 'system', 'channel.added', 'channel', ch.id, { name, phoneNumber, kind: 'whatsapp' });
    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, 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, 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, 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);
    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, 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;
  },

  async terminateSession(id: string, channelKind: ChannelKind = 'whatsapp'): Promise<void> {
    if (channelKind === 'whatsapp') {
      try {
        await fetch(`${WA_WORKER_URL}/sessions/${id}`, { method: 'DELETE' });
      } catch {
        /* worker may not be running */
      }
    }
    await sql`
      UPDATE channels SET status = 'disconnected', session_data = NULL WHERE id = ${id}
    `;
  },

  async clearWorkerSession(id: string): Promise<void> {
    try {
      await fetch(`${WA_WORKER_URL}/sessions/${id}`, { method: 'DELETE' });
    } catch {
      /* Worker down or no session */
    }
  },

  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 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, phone: string, text: string): Promise<string | null> {
    const res = await fetch(`${WA_WORKER_URL}/send`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channelId, phone, text }),
    });

    if (!res.ok) {
      const body = await res.text().catch(() => '');
      throw new Error(`Worker send failed for ${channelId}: ${res.status} ${body}`);
    }

    const data = await res.json() as { ok: boolean; waMessageId: string | null };
    return data.waMessageId ?? null;
  },

  async startSession(channelId: string, _io: SocketServer): Promise<void> {
    const res = await fetch(`${WA_WORKER_URL}/sessions`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ channelId }),
    });

    if (!res.ok) {
      const body = await res.text().catch(() => '');
      throw new Error(`Worker rejected session start for ${channelId}: ${res.status} ${body}`);
    }
  },
};
