import {
  calendarDefaultsForEventType,
  mergeChannelSettings,
  mergeTicketIntake,
  type CalendarEventType,
  type CalendarRequest,
  type ChannelSettings,
  type Ticket,
} from '@wa-ticketing/shared';
import type { FastifyBaseLogger } from 'fastify';
import type { Server as SocketServer } from 'socket.io';
import { sql } from '../db.js';
import { aiAgentService, type AiAgentSecretRow } from './aiAgentService.js';
import { calendarService } from './calendarService.js';
import { channelService } from './channelService.js';
import { completeChat } from './llmProviderService.js';
import { chatCompletionTextWithCredentials } from './openaiService.js';
import { isAnyPlatformLlmConfigured, platformChatCompletionText } from './llmProviderService.js';
import { OPENROUTER_DEFAULT_BASE, openRouterRequestHeaders } from './openrouterService.js';
import { ticketService } from './ticketService.js';

const OPENAI_DEFAULT_BASE = 'https://api.openai.com/v1';
const CONFIDENCE_THRESHOLD = 0.7;
/** Default business timezone for relative phrases like "tomorrow at 12" */
const SCHEDULING_TZ = 'Africa/Johannesburg';

type LlmExtraction = {
  hasSchedulingIntent: boolean;
  eventType?: CalendarEventType;
  /** True when customer did not say site visit vs Teams/online */
  typeAmbiguous?: boolean;
  startsAt?: string;
  endsAt?: string;
  summary?: string;
  confidence?: number;
};

export type SchedulingHandleResult = {
  handled: boolean;
  created: CalendarRequest | null;
  customerReply: string | null;
};

function parseExtractionJson(raw: string): LlmExtraction | null {
  const trimmed = raw.trim();
  const start = trimmed.indexOf('{');
  const end = trimmed.lastIndexOf('}');
  if (start < 0 || end <= start) return null;
  try {
    const obj = JSON.parse(trimmed.slice(start, end + 1)) as Record<string, unknown>;
    return {
      hasSchedulingIntent: Boolean(obj.hasSchedulingIntent),
      eventType: obj.eventType === 'meetup' ? 'meetup' : obj.eventType === 'call_out' ? 'call_out' : undefined,
      typeAmbiguous: Boolean(obj.typeAmbiguous),
      startsAt: typeof obj.startsAt === 'string' ? obj.startsAt : undefined,
      endsAt: typeof obj.endsAt === 'string' ? obj.endsAt : undefined,
      summary: typeof obj.summary === 'string' ? obj.summary : undefined,
      confidence: typeof obj.confidence === 'number' ? obj.confidence : undefined,
    };
  } catch {
    return null;
  }
}

async function resolveLlm(opts: {
  companyId: string | null;
  settings: ChannelSettings;
}): Promise<{ ok: true; agent: AiAgentSecretRow | null; model: string } | { ok: false }> {
  if (opts.companyId) {
    const agentRow = await aiAgentService.getDefaultAgentForCompany(opts.companyId);
    const runnable = await aiAgentService.isRunnableForCompany(opts.companyId);
    if (!runnable) return { ok: false };
    if (agentRow?.api_key?.trim()) {
      return { ok: true, agent: agentRow, model: agentRow.model_id };
    }
    return { ok: true, agent: null, model: opts.settings.ai.model };
  }
  if (!(await isAnyPlatformLlmConfigured())) return { ok: false };
  return { ok: true, agent: null, model: opts.settings.ai.model };
}

async function callSchedulingLlm(opts: {
  system: string;
  userPrompt: string;
  agent: AiAgentSecretRow | null;
  model: string;
}): Promise<string> {
  const messages = [{ role: 'user' as const, content: opts.userPrompt }];

  if (opts.agent?.api_key?.trim()) {
    if (
      opts.agent.provider === 'openai' ||
      opts.agent.provider === 'openai_compatible' ||
      opts.agent.provider === 'openrouter'
    ) {
      const base =
        opts.agent.provider === 'openai'
          ? OPENAI_DEFAULT_BASE
          : opts.agent.provider === 'openrouter'
            ? OPENROUTER_DEFAULT_BASE
            : (opts.agent.base_url?.trim() || '').replace(/\/$/, '');
      const isOpenRouter = opts.agent.provider === 'openrouter';
      return chatCompletionTextWithCredentials({
        base,
        apiKey: opts.agent.api_key,
        model: opts.agent.model_id,
        system: opts.system,
        messages,
        maxTokens: 400,
        responseFormat: 'json_object',
        extraHeaders: isOpenRouter ? openRouterRequestHeaders() : undefined,
        providerLabel: isOpenRouter ? 'OpenRouter' : undefined,
      });
    }
    return completeChat({
      provider: opts.agent.provider,
      apiKey: opts.agent.api_key,
      baseUrl: opts.agent.base_url,
      model: opts.agent.model_id,
      system: opts.system,
      messages,
      maxTokens: 400,
    });
  }

  return platformChatCompletionText({
    model: opts.model,
    system: opts.system,
    messages,
    maxTokens: 400,
    responseFormat: 'json_object',
  });
}

function formatLocalSlot(iso: string): string {
  try {
    return new Intl.DateTimeFormat('en-ZA', {
      timeZone: SCHEDULING_TZ,
      weekday: 'short',
      day: 'numeric',
      month: 'short',
      hour: '2-digit',
      minute: '2-digit',
      hour12: false,
    }).format(new Date(iso));
  } catch {
    return new Date(iso).toISOString();
  }
}

function buildCustomerReply(opts: {
  startsAt: string;
  endsAt: string;
  typeAmbiguous: boolean;
  eventType: CalendarEventType;
  availabilityNote: string;
}): string {
  const when = formatLocalSlot(opts.startsAt);
  const typeLine = opts.typeAmbiguous
    ? 'Is this a *site call-out* (technician visit) or a *Teams / online meeting*? Please reply with “call-out” or “Teams”.'
    : opts.eventType === 'meetup'
      ? 'I’ve logged this as a Teams / online meeting request.'
      : 'I’ve logged this as a site call-out request.';

  return [
    `Thanks — I’ve noted a booking request for *${when}* (${SCHEDULING_TZ.replace('_', ' ')}).`,
    opts.availabilityNote,
    typeLine,
    'Our team will confirm the booking shortly (it is not confirmed until a staff member books it).',
  ].join('\n\n');
}

/**
 * Detect scheduling intent, create a suggested calendar row, and return a customer-facing reply.
 * Call from talkback *before* AI so booking messages are not answered with KB deflection.
 */
export const schedulingIntentService = {
  /** Fire-and-forget wrapper (legacy); prefer tryHandleInbound from talkback. */
  detectAndSuggest(opts: {
    io: SocketServer;
    log: FastifyBaseLogger;
    ticket: Ticket;
    messageId: string;
    inboundText: string;
  }): void {
    void this.tryHandleInbound(opts).catch(err => {
      opts.log.warn({ err, ticketId: opts.ticket.id }, '[calendar] scheduling intent failed');
    });
  },

  async tryHandleInbound(opts: {
    io: SocketServer;
    log: FastifyBaseLogger;
    ticket: Ticket;
    messageId?: string | null;
    inboundText: string;
  }): Promise<SchedulingHandleResult> {
    const empty: SchedulingHandleResult = { handled: false, created: null, customerReply: null };
    const text = opts.inboundText.trim();
    if (!text || text.length < 8) return empty;
    if (/^\[(Image|Voice note|Document)/i.test(text) && text.length < 40) return empty;

    const intake = mergeTicketIntake(opts.ticket.intake);
    if (['who', 'company', 'query', 'status'].includes(intake.step)) return empty;

    if (await calendarService.hasSuggestedForTicket(opts.ticket.id)) {
      const pending = (
        await calendarService.listRequests({
          ticketId: opts.ticket.id,
          status: 'suggested',
          limit: 1,
        })
      )[0];

      const clarifyCallOut = /\b(call[- ]?out|site visit|come out|come over|on[- ]?site)\b/i.test(text);
      const clarifyTeams = /\b(teams|zoom|online|remote|virtual)\b/i.test(text);
      if (pending && (clarifyCallOut || clarifyTeams) && !(clarifyCallOut && clarifyTeams)) {
        const eventType: CalendarEventType = clarifyTeams ? 'meetup' : 'call_out';
        const defaults = calendarDefaultsForEventType(eventType);
        const startMs = new Date(pending.starts_at).getTime();
        const endsAt = new Date(startMs + defaults.durationMinutes * 60_000).toISOString();
        const updated = await calendarService.updateDraft(pending.id, {
          event_type: eventType,
          summary: clarifyTeams ? 'Teams / online meeting request' : 'Site call-out request',
          ends_at: endsAt,
          buffer_before_minutes: defaults.bufferBeforeMinutes,
          buffer_after_minutes: defaults.bufferAfterMinutes,
        });
        await sql`
          UPDATE calendar_requests
          SET extraction = COALESCE(extraction, '{}'::jsonb) || ${sql.json({ typeAmbiguous: false } as never)},
              updated_at = NOW()
          WHERE id = ${pending.id}
        `;
        opts.io.emit('calendar:updated', { id: pending.id, status: 'suggested', ticketId: opts.ticket.id });
        return {
          handled: true,
          created: updated,
          customerReply: clarifyTeams
            ? 'Thanks — I’ve updated this to a *Teams / online meeting* (45 minutes, no travel time). Our team will confirm shortly.'
            : 'Thanks — I’ve updated this to a *site call-out* (including travel time). Our team will confirm shortly.',
        };
      }

      if (/\b(book|booking|appointment|call[- ]?out|meetup|teams|meeting|schedule)\b/i.test(text)) {
        return {
          handled: true,
          created: null,
          customerReply:
            'We already have a pending booking request on this ticket. Reply “call-out” or “Teams” if you need to clarify the type, or wait for our team to confirm the time.',
        };
      }
      return empty;
    }

    const channel = await channelService.getById(opts.ticket.channel_id);
    if (!channel?.settings) return empty;
    const settings = mergeChannelSettings(channel.settings);

    const [contactRow] = await sql<Array<{ company_id: string | null }>>`
      SELECT company_id FROM contacts WHERE id = ${opts.ticket.contact_id}
    `;
    const companyId = contactRow?.company_id ?? null;

    const llm = await resolveLlm({ companyId, settings });
    if (!llm.ok) return empty;

    const nowIso = new Date().toISOString();
    const system = `You extract scheduling intent from customer support messages (including voice transcripts).
Detect requests to book a time: site technician call-out / visit, OR a Teams / online / remote meeting / meetup.
Ignore vague "sometime" requests without a date or time.

Timezone rules:
- Interpret relative times (tomorrow, Friday at 12) in ${SCHEDULING_TZ} unless the customer specifies another zone.
- Return startsAt / endsAt as ISO 8601 with explicit offset (e.g. 2026-08-06T12:00:00+02:00 for noon in Johannesburg).

Event type rules:
- eventType "call_out" = on-site visit / technician comes out
- eventType "meetup" = Teams / Zoom / online / remote meeting
- If the customer only says "booking" / "appointment" / "meeting" without site vs online cues, set typeAmbiguous=true and eventType="call_out" as a provisional default.

Return JSON only:
- hasSchedulingIntent (boolean)
- eventType ("call_out" or "meetup")
- typeAmbiguous (boolean)
- startsAt (ISO 8601 with offset)
- endsAt (ISO 8601; omit to default — 45 minutes for meetup/Teams, 60 minutes for call_out)
- summary (short label, max 120 chars; do not invent "site visit" if typeAmbiguous)
- confidence (0-1)
Current UTC time: ${nowIso}
No markdown, no preamble.`;

    const userPrompt = `Customer message:\n"""${text.slice(0, 2000)}"""\n\nExtract scheduling intent as JSON.`;

    const raw = await callSchedulingLlm({
      system,
      userPrompt,
      agent: llm.agent,
      model: llm.model,
    });

    const parsed = parseExtractionJson(raw);
    if (!parsed?.hasSchedulingIntent) return empty;

    const confidence = parsed.confidence ?? 0;
    if (confidence < CONFIDENCE_THRESHOLD) return empty;
    if (!parsed.startsAt) return empty;

    const startsAt = new Date(parsed.startsAt);
    if (Number.isNaN(startsAt.getTime())) return empty;
    if (startsAt.getTime() < Date.now() - 5 * 60_000) return empty;

    const typeAmbiguous = Boolean(parsed.typeAmbiguous) || !parsed.eventType;
    const eventType: CalendarEventType = parsed.eventType === 'meetup' ? 'meetup' : 'call_out';
    const defaults = calendarDefaultsForEventType(eventType);
    const durationMs = defaults.durationMinutes * 60_000;

    let endsAt = parsed.endsAt ? new Date(parsed.endsAt) : new Date(startsAt.getTime() + durationMs);
    if (Number.isNaN(endsAt.getTime()) || endsAt <= startsAt) {
      endsAt = new Date(startsAt.getTime() + durationMs);
    } else if (!parsed.endsAt || Math.abs(endsAt.getTime() - startsAt.getTime() - durationMs) > 20 * 60_000) {
      // Missing or wildly different duration → enforce 45m Teams / 60m call-out
      endsAt = new Date(startsAt.getTime() + durationMs);
    }

    const summary = (
      parsed.summary?.trim() ||
      (typeAmbiguous
        ? 'Booking request (type unclear)'
        : eventType === 'meetup'
          ? 'Teams / online meeting request'
          : 'Call-out request')
    ).slice(0, 200);

    const fresh = (await ticketService.getById(opts.ticket.id)) ?? opts.ticket;

    let messageId = opts.messageId ?? null;
    if (!messageId) {
      const [lastMsg] = await sql<Array<{ id: string }>>`
        SELECT id FROM messages
        WHERE ticket_id = ${fresh.id} AND direction = 'inbound'
        ORDER BY sequence DESC LIMIT 1
      `;
      messageId = lastMsg?.id ?? null;
    }

    const created = await calendarService.createSuggested({
      ticketId: fresh.id,
      messageId,
      channelId: fresh.channel_id,
      contactId: fresh.contact_id,
      eventType,
      summary,
      startsAt: startsAt.toISOString(),
      endsAt: endsAt.toISOString(),
      assignedTo: fresh.assigned_to,
      bufferBeforeMinutes: defaults.bufferBeforeMinutes,
      bufferAfterMinutes: defaults.bufferAfterMinutes,
      extraction: {
        confidence,
        raw: parsed,
        typeAmbiguous,
        timezone: SCHEDULING_TZ,
        source_snippet: text.slice(0, 400),
        model: llm.model,
      },
    });

    if (!created) return empty;

    // Availability preview for assignee (or note that staff will assign)
    let availabilityNote: string;
    if (created.assigned_to) {
      const avail = await calendarService.checkAvailability({
        assignedTo: created.assigned_to,
        startsAt: created.starts_at,
        endsAt: created.ends_at,
        bufferBeforeMinutes: created.buffer_before_minutes,
        bufferAfterMinutes: created.buffer_after_minutes,
        excludeId: created.id,
      });
      const bufferHint =
        eventType === 'meetup'
          ? 'including 15 minutes prep after'
          : 'including travel time';
      availabilityNote = avail.available
        ? `That slot looks free on our calendar (${bufferHint}).`
        : 'That slot may clash with an existing booking — our team will suggest an alternative if needed.';
    } else {
      availabilityNote = 'Our team will check availability and assign someone when confirming.';
    }

    const customerReply = buildCustomerReply({
      startsAt: created.starts_at,
      endsAt: created.ends_at,
      typeAmbiguous,
      eventType,
      availabilityNote,
    });

    opts.io.emit('calendar:updated', {
      id: created.id,
      status: created.status,
      ticketId: created.ticket_id,
    });
    opts.log.info(
      { calendarRequestId: created.id, ticketId: fresh.id, confidence, typeAmbiguous },
      '[calendar] suggested scheduling request',
    );

    return { handled: true, created, customerReply };
  },
};
