import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma, IiosMeetingType, IiosConsentStatus } from '@prisma/client';
import { CloudEvent, IIOS_EVENTS, IiosPlatformPorts } from '@insignia/iios-contracts';
import { currentTraceId, newTraceId, toTraceparent } from '../observability/trace-context';
import { recordAudit } from '../observability/audit';
import { PrismaService } from '../prisma/prisma.service';
import { PLATFORM_PORTS } from '../platform/platform-ports';
import { decideOrThrow } from '../platform/fail-closed';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { AiJobService } from '../ai/ai.service';
import { OutboundService } from '../adapters/outbound.service';
import { checkMeetingConsent } from './calendar-consent';

export interface AttendeeInput {
  userId: string;
  displayName?: string;
  role?: 'REQUIRED' | 'OPTIONAL';
  visibility?: 'FULL' | 'LIMITED' | 'NONE';
}

export interface RequestMeetingInput {
  interactionId?: string;
  targetActorId?: string;
  requestedWindow: Record<string, unknown>;
  meetingType?: IiosMeetingType;
  callbackRequestId?: string;
  sourceClaimId?: string;
}

export interface ScheduleInput {
  requestId?: string;
  meetingType: IiosMeetingType;
  title: string;
  startAt: string;
  endAt?: string;
  timezone?: string;
  attendees?: AttendeeInput[];
}

/**
 * Calendar & Meeting SDK (P8). A meeting is an interaction specialization: it owns
 * calendar semantics and imports message/inbox. Meetings arrive via three genesis
 * paths (callback bridge / direct / accepted AI EVENT claim) that converge on a
 * meeting_request → schedule. Recording artifacts are gated elsewhere (consent).
 */
@Injectable()
export class CalendarService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
    private readonly actors: ActorResolver,
    private readonly ai: AiJobService,
    private readonly outbound: OutboundService,
  ) {}

  // ── Genesis: meeting requests ────────────────────────────────────
  async requestMeeting(principal: MessagePrincipal, input: RequestMeetingInput) {
    const scope = await this.actors.resolveScope(principal);
    const requester = await this.actors.resolveActor(scope.id, principal);
    const request = await this.prisma.iiosMeetingRequest.create({
      data: {
        scopeId: scope.id,
        interactionId: input.interactionId,
        requestedByActorId: requester.id,
        targetActorId: input.targetActorId,
        requestedWindowJson: input.requestedWindow as Prisma.InputJsonValue,
        meetingType: input.meetingType ?? 'CALLBACK',
        callbackRequestId: input.callbackRequestId,
        sourceClaimId: input.sourceClaimId,
      },
    });
    await this.emit(IIOS_EVENTS.meetingRequested, scope.id, request.id, { requestId: request.id });
    return request;
  }

  /** Callback→meeting bridge (the Atlas's headline path). */
  async fromCallback(principal: MessagePrincipal, callbackId: string, requestedWindow: Record<string, unknown>) {
    const callback = await this.prisma.iiosCallbackRequest.findUnique({ where: { id: callbackId } });
    if (!callback) throw new NotFoundException('callback request not found');
    return this.requestMeeting(principal, {
      requestedWindow,
      meetingType: 'CALLBACK',
      callbackRequestId: callback.id,
    });
  }

  /** P7→P8 tie: an ACCEPTED AI EVENT claim becomes a meeting request (human-confirmed). */
  async fromEventClaim(principal: MessagePrincipal, claimId: string, requestedWindow?: Record<string, unknown>) {
    const claim = await this.prisma.iiosAiClaim.findUnique({ where: { id: claimId } });
    if (!claim || claim.claimType !== 'EVENT') throw new NotFoundException('AI EVENT claim not found');
    if (claim.validationStatus !== 'ACCEPTED') {
      throw new BadRequestException('AI event claim must be human-accepted before it can become a meeting');
    }
    const json = (claim.claimJson ?? {}) as { title?: string; when?: string };
    return this.requestMeeting(principal, {
      requestedWindow: requestedWindow ?? { when: json.when ?? 'unspecified' },
      meetingType: 'INTERNAL',
      sourceClaimId: claim.id,
    });
  }

  // ── Scheduling ───────────────────────────────────────────────────
  async schedule(principal: MessagePrincipal, input: ScheduleInput) {
    const scope = await this.actors.resolveScope(principal);
    await decideOrThrow(this.ports, { action: 'iios.meeting.schedule', scopeId: scope.id, meetingType: input.meetingType });
    const organizer = await this.actors.resolveActor(scope.id, principal);

    const meeting = await this.prisma.iiosMeeting.create({
      data: {
        scopeId: scope.id,
        meetingType: input.meetingType,
        status: 'SCHEDULED',
        organizerActorId: organizer.id,
        title: input.title,
        startAt: new Date(input.startAt),
        endAt: input.endAt ? new Date(input.endAt) : undefined,
        timezone: input.timezone ?? 'UTC',
      },
    });

    // Organizer is a full-visibility participant.
    await this.prisma.iiosMeetingParticipant.create({
      data: { meetingId: meeting.id, actorRefId: organizer.id, role: 'ORGANIZER', attendanceStatus: 'ACCEPTED' },
    });
    for (const a of input.attendees ?? []) {
      const actor = await this.actors.resolveActor(scope.id, {
        userId: a.userId,
        orgId: principal.orgId,
        appId: principal.appId,
        displayName: a.displayName,
      });
      await this.prisma.iiosMeetingParticipant.upsert({
        where: { meetingId_actorRefId: { meetingId: meeting.id, actorRefId: actor.id } },
        create: { meetingId: meeting.id, actorRefId: actor.id, role: a.role ?? 'REQUIRED', visibility: a.visibility ?? 'FULL' },
        update: {},
      });
    }

    // A meeting owns a calendar event (the neutral event record).
    const calEvent = await this.prisma.iiosCalendarEvent.create({
      data: {
        scopeId: scope.id,
        title: input.title,
        startAt: new Date(input.startAt),
        endAt: input.endAt ? new Date(input.endAt) : new Date(input.startAt),
        status: 'CONFIRMED',
      },
    });
    await this.prisma.iiosMeeting.update({ where: { id: meeting.id }, data: { calendarEventId: calEvent.id } });

    // If this came from a request, close the loop — and bridge a callback if present.
    if (input.requestId) {
      const request = await this.prisma.iiosMeetingRequest.update({
        where: { id: input.requestId },
        data: { status: 'SCHEDULED', resultingMeetingId: meeting.id },
      });
      if (request.callbackRequestId) {
        await this.prisma.iiosCallbackRequest.update({
          where: { id: request.callbackRequestId },
          data: { meetingRef: meeting.id },
        });
      }
    }

    await this.emit(IIOS_EVENTS.meetingScheduled, scope.id, meeting.id, { meetingId: meeting.id });
    await this.queueReminders(meeting.id);
    return this.getMeeting(meeting.id);
  }

  /**
   * Queue a meeting reminder: one sandbox notification + a SYSTEM_ALERT inbox item
   * per participant. Idempotent — the outbound key dedupes the notification and the
   * per-participant inbox item is guarded by the meeting marker.
   */
  async queueReminders(meetingId: string): Promise<void> {
    const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId }, include: { participants: true } });
    if (!meeting) return;
    const cmd = await this.outbound.send(
      'PORTAL',
      `meeting:${meeting.id}`,
      { text: `Reminder: "${meeting.title}"` },
      `meeting-reminder:${meeting.id}`,
      meeting.scopeId,
      'meeting_reminder',
    );
    for (const p of meeting.participants) {
      if (!p.actorRefId) continue;
      const exists = await this.prisma.iiosInboxItem.count({
        where: { ownerActorId: p.actorRefId, kind: 'SYSTEM_ALERT', actionRef: { path: ['meetingId'], equals: meeting.id } },
      });
      if (exists > 0) continue;
      await this.prisma.iiosInboxItem.create({
        data: {
          scopeId: meeting.scopeId,
          ownerActorId: p.actorRefId,
          kind: 'SYSTEM_ALERT',
          state: 'OPEN',
          title: `Upcoming meeting: ${meeting.title}`,
          actionRef: { meetingId: meeting.id },
        },
      });
    }
    await this.emit(IIOS_EVENTS.meetingReminderQueued, meeting.scopeId, meeting.id, { meetingId: meeting.id, commandId: cmd.id });
  }

  /** Direct genesis: request + schedule in one call. */
  async scheduleDirect(principal: MessagePrincipal, input: ScheduleInput) {
    const request = await this.requestMeeting(principal, {
      requestedWindow: { startAt: input.startAt, endAt: input.endAt },
      meetingType: input.meetingType,
    });
    return this.schedule(principal, { ...input, requestId: request.id });
  }

  async listMeetings(principal: MessagePrincipal) {
    const scope = await this.actors.resolveScope(principal);
    return this.prisma.iiosMeeting.findMany({
      where: { scopeId: scope.id },
      orderBy: { createdAt: 'desc' },
      take: 100,
      include: { participants: true },
    });
  }

  async getMeeting(id: string, principal?: MessagePrincipal) {
    const meeting = await this.prisma.iiosMeeting.findUnique({
      where: { id },
      include: {
        participants: true,
        transcripts: { include: { segments: true } },
        summaries: true,
        actionItems: { include: { actionItem: true } },
      },
    });
    if (!meeting) throw new NotFoundException('meeting not found');
    if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02)
    return meeting;
  }

  // ── Consent + transcript + summary (the KG-14 safety spine) ──────
  async setConsent(meetingId: string, actorRefId: string, status: IiosConsentStatus, principal?: MessagePrincipal) {
    if (principal) {
      const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId } });
      if (!meeting) throw new NotFoundException('meeting not found');
      await this.actors.assertOwns(principal, meeting.scopeId);
    }
    return this.prisma.iiosMeetingParticipant.update({
      where: { meetingId_actorRefId: { meetingId, actorRefId } },
      data: { recordingConsent: status },
    });
  }

  /**
   * Create the transcript ONLY if every attendee consented + CMP allows; otherwise
   * a BLOCKED transcript with no segments (fail-closed, KG-14). Idempotent per meeting.
   */
  async generateTranscript(meetingId: string, principal?: MessagePrincipal) {
    const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId }, include: { participants: true } });
    if (!meeting) throw new NotFoundException('meeting not found');
    if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02)

    const existing = await this.prisma.iiosMeetingTranscript.findFirst({ where: { meetingId }, include: { segments: true } });
    if (existing && existing.status === 'READY') return existing;

    const verdict = await checkMeetingConsent(this.ports, meetingId, meeting.participants);
    if (!verdict.allowed) {
      return this.prisma.iiosMeetingTranscript.create({
        data: { meetingId, transcriptRef: `transcript:${meetingId}`, status: 'BLOCKED' },
        include: { segments: true },
      });
    }

    const transcript = await this.prisma.iiosMeetingTranscript.create({
      data: { meetingId, transcriptRef: `transcript:${meetingId}`, status: 'READY', consentRefId: verdict.receiptRef },
    });
    let seg = 0;
    for (const p of meeting.participants) {
      await this.prisma.iiosTranscriptSegment.create({
        data: {
          transcriptId: transcript.id,
          segmentNo: seg,
          speakerActorId: p.actorRefId ?? undefined,
          startMs: seg * 1000,
          endMs: (seg + 1) * 1000,
          contentRef: `Speaker ${seg + 1}: discussed "${meeting.title}" and agreed to follow up.`,
          confidence: 0.9,
        },
      });
      seg++;
    }
    return this.prisma.iiosMeetingTranscript.findUniqueOrThrow({ where: { id: transcript.id }, include: { segments: true } });
  }

  /**
   * Summarize a READY transcript via the P7 AI layer + derive action items and
   * per-attendee inbox follow-ups. Re-checks consent live (defense in depth) so a
   * revoked consent blocks summarization even if a transcript already exists.
   * Attendees with visibility=NONE are excluded from inbox exposure (Scenario 6).
   */
  async summarize(meetingId: string, principal?: MessagePrincipal) {
    const meeting = await this.prisma.iiosMeeting.findUnique({
      where: { id: meetingId },
      include: { participants: true, transcripts: { include: { segments: true } } },
    });
    if (!meeting) throw new NotFoundException('meeting not found');
    if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02)

    const verdict = await checkMeetingConsent(this.ports, meetingId, meeting.participants);
    if (!verdict.allowed) throw new BadRequestException(`recording consent not satisfied: ${verdict.reason}`);

    const transcript = meeting.transcripts.find((t) => t.status === 'READY');
    if (!transcript) throw new BadRequestException('no READY transcript to summarize');

    const already = await this.prisma.iiosMeetingSummary.findFirst({ where: { meetingId } });
    if (already) return this.getMeeting(meetingId);

    const scopeId = meeting.scopeId;
    const text = transcript.segments.map((s) => s.contentRef).join(' ');
    const idk = `meeting-transcript:${meetingId}`;
    const interaction =
      (await this.prisma.iiosInteraction.findFirst({ where: { scopeId, idempotencyKey: idk } })) ??
      (await this.prisma.iiosInteraction.create({
        data: { scopeId, kind: 'SUMMARY', idempotencyKey: idk, parts: { create: [{ partIndex: 0, kind: 'TEXT', bodyText: text }] } },
      }));

    const sum = await this.ai.runJob({ interactionId: interaction.id, jobType: 'SUMMARIZE' });
    const ext = await this.ai.runJob({ interactionId: interaction.id, jobType: 'EXTRACT' });

    const summary = await this.prisma.iiosMeetingSummary.create({
      data: { meetingId, aiArtifactId: sum.artifact?.id, summaryType: 'AI', status: 'PROPOSED' },
    });

    const claims = ext.artifact
      ? await this.prisma.iiosAiClaim.findMany({ where: { artifactId: ext.artifact.id, claimType: 'EVENT' } })
      : [];
    const titles = claims.length > 0 ? claims.map((c) => String((c.claimJson as { title?: string }).title ?? 'follow-up')) : ['Meeting follow-up'];
    for (const title of titles) {
      const item = await this.prisma.iiosActionItem.create({
        data: { scopeId, ownerActorId: meeting.organizerActorId, title, status: 'OPEN', sourceInteractionId: interaction.id },
      });
      await this.prisma.iiosMeetingActionItem.create({
        data: { meetingId, actionItemId: item.id, ownerActorId: meeting.organizerActorId },
      });
    }

    // Per-attendee inbox follow-up — attendee-level visibility gates exposure.
    for (const p of meeting.participants) {
      if (p.visibility === 'NONE' || !p.actorRefId) continue;
      await this.prisma.iiosInboxItem.create({
        data: { scopeId, ownerActorId: p.actorRefId, kind: 'MEETING_FOLLOWUP', state: 'OPEN', title: `Follow-up: ${meeting.title}` },
      });
    }

    await this.emit(IIOS_EVENTS.meetingSummarized, scopeId, meetingId, { meetingId, summaryId: summary.id });
    await recordAudit(this.prisma, { action: 'meeting.summarized', resourceType: 'meeting', resourceId: meetingId, scopeId });
    return this.getMeeting(meetingId);
  }

  async approveSummary(summaryId: string, principal: MessagePrincipal) {
    const summary = await this.prisma.iiosMeetingSummary.findUnique({ where: { id: summaryId }, include: { meeting: true } });
    if (!summary) throw new NotFoundException('summary not found');
    const actor = await this.actors.resolveActor(summary.meeting.scopeId, principal);
    return this.prisma.iiosMeetingSummary.update({
      where: { id: summaryId },
      data: { status: 'APPROVED', approvedByActorId: actor.id, publishedAt: new Date() },
    });
  }

  async listActionItems(meetingId: string, principal?: MessagePrincipal) {
    if (principal) {
      const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId } });
      if (!meeting) throw new NotFoundException('meeting not found');
      await this.actors.assertOwns(principal, meeting.scopeId);
    }
    return this.prisma.iiosMeetingActionItem.findMany({ where: { meetingId }, include: { actionItem: true } });
  }

  protected async emit(type: string, scopeId: string, aggregateId: string, data: Record<string, unknown>): Promise<void> {
    const tid = currentTraceId() ?? newTraceId();
    const event: CloudEvent = {
      specversion: '1.0',
      id: `evt_cal_${aggregateId}_${type.split('.').slice(-2, -1)[0]}`,
      type,
      source: `iios/calendar/${scopeId}`,
      subject: `meeting/${aggregateId}`,
      time: new Date().toISOString(),
      datacontenttype: 'application/json',
      traceparent: toTraceparent(tid),
      insignia: { scopeSnapshotId: scopeId, correlationId: tid, idempotencyKey: `${type}:${aggregateId}`, dataClass: 'internal' },
      data,
    };
    await this.prisma.iiosOutboxEvent.create({
      data: {
        aggregateType: 'meeting',
        aggregateId,
        eventType: type,
        cloudEvent: event as unknown as Prisma.InputJsonValue,
        partitionKey: `${scopeId}:${aggregateId}`,
      },
    });
  }
}
