import { env } from '../config/env';
import { logger } from '../utils/logger';

// --- Types ---

export interface GancioEvent {
  id: number;
  title: string;
  description: string;
  place_name: string;
  place_address: string;
  start_datetime: number; // Unix timestamp
  end_datetime?: number;
  tags: string[];
}

interface GancioLoginResponse {
  access_token: string;
  token_type: string;
}

// --- Client ---

class GancioClient {
  private accessToken: string | null = null;
  private tokenExpiresAt = 0;

  private get baseUrl(): string {
    return env.GANCIO_URL;
  }

  get enabled(): boolean {
    return env.GANCIO_SYNC_ENABLED === 'true' && !!env.GANCIO_ADMIN_PASSWORD;
  }

  /**
   * Authenticate with Gancio OAuth password grant, cache token for 1 hour.
   * Gancio uses POST /oauth/login with application/x-www-form-urlencoded body
   * (oauth2orize standard). Fields: username, password, client_id="self", grant_type="password".
   */
  private async login(): Promise<void> {
    if (this.accessToken && Date.now() < this.tokenExpiresAt) return;

    const url = `${this.baseUrl}/oauth/login`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10000);

    // Gancio's oauth2orize endpoint requires URL-encoded form data (not JSON)
    const formBody = new URLSearchParams({
      username: env.GANCIO_ADMIN_USER,
      password: env.GANCIO_ADMIN_PASSWORD,
      client_id: 'self',
      grant_type: 'password',
    });

    try {
      const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: formBody.toString(),
        signal: controller.signal,
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        throw new Error(`Gancio login failed (${res.status}): ${text}`);
      }

      const data = await res.json() as GancioLoginResponse;
      this.accessToken = data.access_token;
      // Cache for 1 hour
      this.tokenExpiresAt = Date.now() + 60 * 60 * 1000;
      logger.debug('Gancio auth token refreshed');
    } finally {
      clearTimeout(timeout);
    }
  }

  /**
   * Make an authenticated JSON request to the Gancio API
   */
  private async request<T>(
    method: string,
    path: string,
    body?: Record<string, unknown>,
  ): Promise<T> {
    await this.login();

    const url = `${this.baseUrl}${path}`;
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 10000);

    const headers: Record<string, string> = {};
    if (this.accessToken) {
      headers['Authorization'] = `Bearer ${this.accessToken}`;
    }

    let fetchBody: string | undefined;
    if (body) {
      headers['Content-Type'] = 'application/json';
      fetchBody = JSON.stringify(body);
    }

    try {
      const res = await fetch(url, {
        method,
        headers,
        body: fetchBody,
        signal: controller.signal,
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        throw new Error(`Gancio API ${method} ${path} returned ${res.status}: ${text}`);
      }

      const contentType = res.headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        return await res.json() as T;
      }
      return {} as T;
    } finally {
      clearTimeout(timeout);
    }
  }

  // --- Health ---

  async isAvailable(): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      try {
        const res = await fetch(`${this.baseUrl}/api/events`, {
          signal: controller.signal,
        });
        return res.ok;
      } finally {
        clearTimeout(timeout);
      }
    } catch {
      return false;
    }
  }

  // --- Event CRUD ---

  /**
   * Create a Gancio event from a shift
   */
  async createEvent(event: {
    title: string;
    description?: string | null;
    location?: string | null;
    date: Date;
    startTime: string;
    endTime: string;
    tags?: string[];
  }): Promise<number | null> {
    if (!this.enabled) return null;

    try {
      const startDatetime = this.buildTimestamp(event.date, event.startTime);
      const endDatetime = this.buildTimestamp(event.date, event.endTime);
      const placeName = event.location || 'TBD';

      const created = await this.request<GancioEvent>('POST', '/api/event', {
        title: event.title,
        description: event.description || '',
        place_name: placeName,
        place_address: event.location || placeName,
        start_datetime: startDatetime,
        end_datetime: endDatetime,
        tags: event.tags ?? ['volunteer', 'shift'],
      });

      logger.info(`Gancio: created event ${created.id} for "${event.title}"`);
      return created.id;
    } catch (err) {
      logger.warn('Gancio createEvent failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Update an existing Gancio event
   */
  async updateEvent(eventId: number, shift: {
    title: string;
    description?: string | null;
    location?: string | null;
    date: Date;
    startTime: string;
    endTime: string;
  }): Promise<void> {
    if (!this.enabled) return;

    try {
      const startDatetime = this.buildTimestamp(shift.date, shift.startTime);
      const endDatetime = this.buildTimestamp(shift.date, shift.endTime);
      const placeName = shift.location || 'TBD';

      await this.request('PUT', '/api/event', {
        id: eventId,
        title: shift.title,
        description: shift.description || '',
        place_name: placeName,
        place_address: shift.location || placeName,
        start_datetime: startDatetime,
        end_datetime: endDatetime,
        tags: ['volunteer', 'shift'],
      });

      logger.info(`Gancio: updated event ${eventId}`);
    } catch (err) {
      logger.warn(`Gancio updateEvent(${eventId}) failed:`, err instanceof Error ? err.message : err);
    }
  }

  /**
   * Delete a Gancio event
   */
  async deleteEvent(eventId: number): Promise<void> {
    if (!this.enabled) return;

    try {
      await this.request('DELETE', `/api/event/${eventId}`);
      logger.info(`Gancio: deleted event ${eventId}`);
    } catch (err) {
      logger.warn(`Gancio deleteEvent(${eventId}) failed:`, err instanceof Error ? err.message : err);
    }
  }

  // --- Public Fetch (no auth needed) ---

  /**
   * Fetch events from Gancio's public API (no auth required).
   * Returns raw Gancio events array, or empty array on failure.
   */
  async fetchPublicEvents(): Promise<GancioEvent[]> {
    try {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 5000);
      try {
        const res = await fetch(`${this.baseUrl}/api/events`, {
          signal: controller.signal,
        });
        if (!res.ok) return [];
        return await res.json() as GancioEvent[];
      } finally {
        clearTimeout(timeout);
      }
    } catch {
      return [];
    }
  }

  // --- Settings ---

  /**
   * Get all Gancio instance settings (flat JSON object).
   * Returns null on failure (fire-and-forget safe).
   */
  async getSettings(): Promise<Record<string, unknown> | null> {
    if (!this.enabled) return null;

    try {
      return await this.request<Record<string, unknown>>('GET', '/api/settings');
    } catch (err) {
      logger.warn('Gancio getSettings failed:', err instanceof Error ? err.message : err);
      return null;
    }
  }

  /**
   * Set a single Gancio setting. Gancio's API only accepts one key/value per call.
   * Returns true on success, false on failure.
   */
  async setSetting(key: string, value: unknown): Promise<boolean> {
    if (!this.enabled) return false;

    try {
      await this.request('POST', '/api/settings', { key, value });
      return true;
    } catch (err) {
      logger.warn(`Gancio setSetting("${key}") failed:`, err instanceof Error ? err.message : err);
      return false;
    }
  }

  // --- Helpers ---

  /**
   * Combine a Date and "HH:MM" time string into a Unix timestamp (seconds)
   */
  private buildTimestamp(date: Date, time: string): number {
    const d = new Date(date);
    const [h, m] = time.split(':').map(Number);
    d.setHours(h || 0, m || 0, 0, 0);
    return Math.floor(d.getTime() / 1000);
  }
}

export const gancioClient = new GancioClient();
