import axios from "axios";
import winston from "winston";
import fs from "fs";
import { google, calendar_v3, meet_v2 } from "googleapis";
import { OAuth2Client } from "google-auth-library";

import {
  Appointment,
  CRMBackend,
  FreeBusyResponse,
  GoogleCalendarEvent,
} from "../crm.interfaces.js";
import { appointmentType, appointmentStatus } from "../crm.types.js";

// Environment variables
const GOOGLE_CALENDAR_OWNER = process.env.GOOGLE_CALENDAR_OWNER;
const SIPGATE_TOKEN_ID = process.env.SIPGATE_TOKEN_ID;
const SIPGATE_TOKEN = process.env.SIPGATE_TOKEN;
const GOOGLE_APPLICATION_CREDENTIALS = process.env.GOOGLE_APPLICATION_CREDENTIALS;

export class GoogleCalendarBackend implements CRMBackend {
  // Constants
  private readonly SIPGATE_BASE_URL = 'https://api.sipgate.com/v2';
  private readonly DEFAULT_TIMEZONE = 'Europe/Berlin';
  private readonly APPOINTMENT_DURATION_MS = 60 * 60 * 1000; // 1 hour in milliseconds
  private readonly DEFAULT_DELEGATION_EMAIL = "konrad@lautmaler.de";
  
  // Services
  private logger: winston.Logger = winston.createLogger();
  private googleAuth: OAuth2Client = new OAuth2Client();
  private calendarService: calendar_v3.Calendar = google.calendar({ version: "v3" });
  private meetService: meet_v2.Meet = google.meet({ version: "v2" });
  
  // Configuration
  private calendarOwnerId: string;
  private userDelegationEmail: string;
  private googleMeetUrl: string = "https://meet.google.com";

  constructor() {
    this.userDelegationEmail = this.DEFAULT_DELEGATION_EMAIL;
    this.calendarOwnerId = GOOGLE_CALENDAR_OWNER || "";
    this.googleAuth = new OAuth2Client();
    
    // Initialize services
    this.initializeLogger();
    this.initializeGoogleServices();
  }

  /**
   * Initialize the logger
   */
  private initializeLogger(): void {
    this.logger = winston.createLogger({
      level: "info",
      format: winston.format.json(),
      defaultMeta: { service: "crm-service" },
      transports: [
        new winston.transports.Console({
          format: winston.format.simple(),
        }),
      ],
    });
  }

  /**
   * Initialize Google services if not in test environment
   */
  private initializeGoogleServices(): void {
    // Skip initialization in test environment
    if (process.env.APP_ENV === "test") {
      this.logger.info("Google services not initialized in test environment");
      return;
    }

    try {
      this.initializeGoogleAuth();
    } catch (error) {
      this.logger.error("Error initializing Google services:", error);
    }
  }

  /**
   * Authenticate with Google services
   */
  private async initializeGoogleAuth(): Promise<void> {
    const credentials = this.parseCredentials();
    this.setupCalendarService(credentials);
    this.setupMeetService(credentials);
  }

  /**
   * Parse the Google service account credentials
   */
  private parseCredentials(): { client_email: string; private_key: string } {
    if (!GOOGLE_APPLICATION_CREDENTIALS) {
      throw new Error("Service account key path is not specified.");
    }

    try {
      const fileContents = fs.readFileSync(GOOGLE_APPLICATION_CREDENTIALS, "utf8");
      const credentials = JSON.parse(fileContents);
      
      if (!credentials.client_email || !credentials.private_key) {
        throw new Error("Invalid credentials format: missing client_email or private_key");
      }
      
      return credentials;
    } catch (error) {
      this.logger.error("Error parsing credentials:", error);
      throw new Error("Failed to parse service account credentials");
    }
  }

  /**
   * Set up the Google Calendar service
   */
  private setupCalendarService(credentials: { client_email: string; private_key: string }): void {
    const auth = new google.auth.JWT({
      email: credentials.client_email,
      key: credentials.private_key,
      scopes: [
        "https://www.googleapis.com/auth/calendar",
        "https://www.googleapis.com/auth/calendar.events",
      ],
      subject: this.userDelegationEmail,
    });

    this.googleAuth = auth;
    this.calendarService = google.calendar({
      version: "v3",
      auth: this.googleAuth,
    });
    
    // Set up ACL rules
    this.setupCalendarAcl();
    this.logger.info("Google Calendar service initialized");
  }

  /**
   * Set up ACL rules for calendar sharing
   */
  private async setupCalendarAcl(): Promise<void> {
    try {
      const aclRules = [
        {
          role: "reader",
          scope: {
            type: "default",
          },
        },
      ];
      
      for (const rule of aclRules) {
        await this.calendarService.acl.insert({
          calendarId: "primary",
          requestBody: rule,
        });
      }
    } catch (error) {
      this.logger.error("Error setting up calendar ACL:", error);
    }
  }

  /**
   * Set up the Google Meet service
   */
  private setupMeetService(credentials: { client_email: string; private_key: string }): void {
    const meetServiceAuth = new google.auth.JWT({
      email: credentials.client_email,
      key: credentials.private_key,
      scopes: [
        "https://www.googleapis.com/auth/calendar",
        "https://www.googleapis.com/auth/meetings.space.created"
      ],
      subject: this.userDelegationEmail,
    });

    this.meetService = google.meet({
      version: "v2",
      auth: meetServiceAuth,
    });
  }

  /**
   * Fetch available time slots based on calendar availability
   */
  async fetchAvailableSlots(
    timestamp: string,
    attendees?: string[],
  ): Promise<string[]> {
    const timeMin = new Date(timestamp).toISOString();
    // set timeMax to 6 hours from the timestamp
    const timeMax = new Date(
      new Date(timestamp).getTime() + 6 * 60 * 60 * 1000
    ).toISOString();
    
    if (!attendees || attendees.length === 0) {
      attendees = [];
    }

    const payload = {
      timeMin,
      timeMax,
      timeZone: this.DEFAULT_TIMEZONE,
      items: [{ id: this.calendarOwnerId }].concat(
        attendees.map((email) => ({ id: email })),
      ),
    };
    
    const busySlots = await this.freeBusyApiCall(payload);
    const totalSlots = this.generateAvailableSlots(timeMin, timeMax, busySlots);
    // return 3 slots
    return totalSlots.slice(0, 3);
  }

  /**
   * Call the FreeBusy API to get busy time slots
   */
  private async freeBusyApiCall(payload: any): Promise<string[]> {
    const freeBusyEndpoint = "https://www.googleapis.com/calendar/v3/freeBusy";
    
    // Ensure we have an access token
    const accessToken = await this.googleAuth.getAccessToken();
    if (!accessToken) {
      throw new Error("Failed to get access token");
    }
    // this.logger.info(`Access token: ${accessToken.token}`);

    try {
      const response = await axios.post(freeBusyEndpoint, payload, {
        headers: {
          Authorization: `Bearer ${accessToken.token}`,
          "Content-Type": "application/json",
        },
      });
      
      const freeBusyResponse = response.data as FreeBusyResponse;
      this.logger.info("FreeBusy API response received successfully");
      
      return freeBusyResponse.calendars[this.calendarOwnerId].busy as string[];
    } catch (error: any) {
      this.logger.error(
        "Error fetching available slots:",
        error.response?.data || error,
      );
      throw new Error("Failed to fetch available slots");
    }
  }

  /**
   * Generate available time slots based on busy times
   */
  private generateAvailableSlots(timeMin: string, timeMax: string, busySlots: any[]): string[] {
    const availableSlots: string[] = [];
    let currentSlot = new Date(timeMin);
    const endTime = new Date(timeMax);
    
    while (currentSlot < endTime) {
      const slotEnd = new Date(currentSlot.getTime() + this.APPOINTMENT_DURATION_MS);
      
      const isSlotBusy = busySlots.some(
        (busySlot) =>
          busySlot.start <= slotEnd.toISOString() &&
          busySlot.end >= currentSlot.toISOString(),
      );
      
      if (!isSlotBusy) {
        availableSlots.push(currentSlot.toISOString());
      }
      
      currentSlot = slotEnd;
    }
    
    return availableSlots;
  }

  /**
   * Fetch available appointment types
   */
  async fetchAppointmentTypes(): Promise<string[]> {
    return ["Consultation", "Meeting", "Demo"];
  }

  /**
   * Book a new appointment
   */
  async bookAppointment(appointment: Appointment): Promise<string> {
    try {
      return this.createEvent(appointment);
    } catch (error) {
      this.logger.error("Error booking appointment:", error);
      throw new Error("Failed to book appointment");
    }
  }

  /**
   * Modify an existing appointment
   */
  async modifyAppointment(
    id: string,
    updatedInfo: Partial<Appointment>,
  ): Promise<Appointment> {
    // Fetch the original appointment
    const originalAppointment = await this.findAppointmentById(id);
    if (!originalAppointment) {
      throw new Error("Appointment not found");
    }
    
    // Merge original and updated info
    const updatedEmployeeEmail = updatedInfo.employeeEmail ?? originalAppointment.employeeEmail;
    const updatedContactEmail = updatedInfo.contactEmail ?? originalAppointment.contactEmail;
    const updatedTimestamp = updatedInfo.timestamp ?? originalAppointment.timestamp;

    if (!updatedEmployeeEmail || !updatedContactEmail) {
      throw new Error("Email cannot be undefined");
    }

    // Create updated event data
    const requestBody: GoogleCalendarEvent = {
      summary: "Meeting",
      start: {
        dateTime: new Date(updatedTimestamp).toISOString(),
        timeZone: "UTC",
      },
      end: {
        dateTime: new Date(
          new Date(updatedTimestamp).getTime() + this.APPOINTMENT_DURATION_MS,
        ).toISOString(),
        timeZone: "UTC",
      },
      attendees: [
        { email: updatedEmployeeEmail },
        { email: updatedContactEmail },
      ],
      conferenceData: {
        createRequest: {
          requestId: updatedTimestamp,
          conferenceSolutionKey: { type: "hangoutsMeet" },
        },
      },
    };
    
    // Update the event in Google Calendar
    await this.calendarService.events.update({
      calendarId: "primary",
      eventId: id,
      requestBody: requestBody,
    });
    
    return { ...originalAppointment, ...updatedInfo };
  }

  /**
   * Find an appointment by contact name (email)
   */
  async findAppointmentByContactName(name: string): Promise<Appointment> {
    const response = await this.calendarService.events.list({
      calendarId: "primary",
    });
    
    const events = response.data.items ?? [];
    const appointments = events
      .filter((event) =>
        event.attendees?.find((attendee) => attendee.email === name),
      )
      .map((event) => this.mapEventToAppointment(event));
    
    if (appointments.length === 0) {
      throw new Error("No appointments found for this contact");
    }
    
    return appointments[0];
  }

  /**
   * Find an appointment by timestamp
   */
  async findAppointmentByTimestamp(timestamp: string): Promise<Appointment | null> {
    throw new Error("Method not implemented.");
  }

  /**
   * Find an appointment by ID
   */
  async findAppointmentById(id: string): Promise<Appointment | null> {
    try {
      const response = await this.calendarService.events.get({
        calendarId: "primary",
        eventId: id,
      });
      
      const event = response.data;
      return this.mapEventToAppointment(event);
    } catch (error) {
      this.logger.error("Error finding appointment by ID:", error);
      return null;
    }
  }

  /**
   * Map a Google Calendar event to an Appointment object
   */
  private mapEventToAppointment(event: any): Appointment {
    if (!event.id || !event.start?.dateTime) {
      throw new Error("Invalid event data");
    }
    
    const attendees = event.attendees || [];
    if (attendees.length < 2) {
      throw new Error("Event has insufficient attendees");
    }
    
    return {
      id: event.id,
      type: "Meeting" as appointmentType,
      timestamp: event.start.dateTime,
      contactEmail: attendees[0]?.email || "",
      contactName: attendees[0]?.displayName || attendees[0]?.email || "",
      employeeEmail: attendees[1]?.email || "",
      status: "Scheduled" as appointmentStatus,
    };
  }

  /**
   * Create a Google Calendar event
   */
  async createEvent(appointment: Appointment): Promise<string> {
    return this.createConference(appointment);
  }

  /**
   * Create a conference event with Google Meet
   */
  private async createConference(appointment: Appointment): Promise<string> {
    try {
      let attendeeEmails = [{ email: appointment.employeeEmail }];
      if (appointment.contactEmail) {
        attendeeEmails.push({ email: appointment.contactEmail });
      }
      // Create the calendar event with conference data
      const startTime = new Date(appointment.timestamp).toISOString();
      const endTime = new Date(
        new Date(appointment.timestamp).getTime() + this.APPOINTMENT_DURATION_MS
      ).toISOString();
      
      // @ts-expect-error overload conflict
      const conference: any = await this.calendarService.events.insert({
        calendarId: "primary",
        anyoneCanAddSelf: true,
        visibility: "public",
        description: this.googleMeetUrl,
        requestBody: {
          summary: `${appointment.employeeName || ""} & ${appointment.contactName || ""}`,
          start: {
            dateTime: startTime,
            timeZone: "UTC",
          },
          end: {
            dateTime: endTime,
            timeZone: "UTC",
          },
          attendees: attendeeEmails,
        },
        conferenceDataVersion: 1,
        sendUpdates: "all",
      });
      
      this.logger.info("Event created: %s", conference.data.htmlLink);
      
      // Extract event ID from HTML link
      const htmlLink = conference.data.htmlLink;
      const eventId = htmlLink.split("?eid=")[1];
      
      return eventId;
    } catch (error) {
      this.logger.error("Error creating conference event:", error);
      throw new Error("Failed to create conference event");
    }
  }

  /**
   * Create an event and send an SMS notification
   */
  async createEventToSms(appointment: Appointment): Promise<string> {
    try {
      // Create Google Meet space
      const conferenceData = await this.meetService.spaces.create({
        requestBody: {
          config: {
            accessType: "OPEN",
            entryPointAccess: "ALL",
          }
        }
      });
      
      const meetingUrl = conferenceData.data.meetingUri || "";
      this.googleMeetUrl = meetingUrl;
      
      // Format SMS message
      const messageContent = this.formatSmsMessage(appointment, meetingUrl);
      
      // Validate phone number
      if (!appointment.contactPhone) {
        throw new Error("Contact phone number is missing");
      }
      
      // Send SMS
      await this.sendSipgateSms(messageContent, appointment.contactPhone);
      
      return "success";
    } catch (error) {
      this.logger.error("Error creating event and sending SMS:", error);
      throw new Error("Failed to create event and send SMS");
    }
  }

  /**
   * Create a calendar event and send an SMS invitation
   */
  async createCalendarEventToSms(appointment: Appointment): Promise<string> {
    try {
      // Create the event
      const eventId = await this.createEvent(appointment);
      this.logger.info(`Event created with ID: ${eventId}`);
      
      // Generate invitation message
      const message = this.formatSmsMessage(appointment, this.googleMeetUrl);
      
      // Validate phone number
      if (!appointment.contactPhone) {
        throw new Error("Contact phone number is missing");
      }
      
      // Send SMS
      await this.sendSipgateSms(message, appointment.contactPhone);
      this.logger.info(`SMS sent to ${appointment.contactPhone}`);
      
      return "success";
    } catch (error) {
      this.logger.error("Error creating calendar event and sending SMS:", error);
      throw new Error("Failed to create calendar event and send SMS");
    }
  }

  /**
   * Format an SMS message for appointment notification
   */
  private formatSmsMessage(appointment: Appointment, meetSpaceUrl: string): string {
    const formattedDate = new Date(appointment.timestamp).toLocaleString("de-DE", {
      weekday: "long",
      year: "numeric",
      month: "long",
      day: "numeric",
      hour: "numeric",
      minute: "numeric",
    });
    
    return [
      `Hallo ${appointment.contactName},`,
      `Vielen Dank für Ihren Anruf.`,
      `Sie haben einen Termin mit ${appointment.employeeName}`,
      `am ${formattedDate}.`,
      `Dem Meeting können Sie über folgenden Link beitreten:`,
      `${meetSpaceUrl}`,
      `Wir freuen uns auf Sie!`,
      `Die Lautmaler GmbH`
    ].join(' ');
  }

  /**
   * Send an SMS via Sipgate API
   */
  private async sendSipgateSms(message: string, recipient: string): Promise<any> {
    this.logger.info('SIPGATE:: verifying credentials');
    
    if (!SIPGATE_TOKEN || !SIPGATE_TOKEN_ID) {
      throw new Error('ERROR: SIPGATE CREDENTIALS MISSING');
    }
    
    const headers = {
      'Content-Type': 'application/json',
    };
    
    const requestBody = {
      smsId: 's9',
      recipient: recipient,
      message: message,
    };
    
    try {
      this.logger.info('SIPGATE:: sending SMS');
      const response = await axios.post(`${this.SIPGATE_BASE_URL}/sessions/sms`, requestBody, {
        headers: headers,
        auth: {
          username: SIPGATE_TOKEN_ID,
          password: SIPGATE_TOKEN,
        },
      });
      
      return response.data;
    } catch (error: any) {
      this.logger.error(`SIPGATE:: error ${error.message}`);
      throw new Error(`Failed to send SMS: ${error.message}`);
    }
  }
}

export default GoogleCalendarBackend;