import { google, type Auth } from 'googleapis';
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
import type { GmailConfig } from '../../config/schema.js';
import type { Tool, ToolResult } from '../types.js';
import { sanitizeHtml } from '../../utils/html.js';

/** Expand ~ to home directory. */
function expandPath(p: string): string {
  if (p.startsWith('~/') || p === '~') {
    return resolve(homedir(), p.slice(2));
  }
  return resolve(p);
}

/** Create an OAuth2 client from Gmail config (credentials + token files). */
function createOAuth2Client(config: NonNullable<GmailConfig>): Auth.OAuth2Client {
  const credentialsPath = config.credentials_file;
  if (!credentialsPath) {
    throw new Error('No credentials_file configured. Set automation.gmail.credentials_file in config.');
  }

  const expandedCredsPath = expandPath(credentialsPath);
  if (!existsSync(expandedCredsPath)) {
    throw new Error(`Credentials file not found: ${expandedCredsPath}`);
  }

  const credentials = JSON.parse(readFileSync(expandedCredsPath, 'utf-8'));
  const { client_id, client_secret, redirect_uris } = credentials.installed ?? credentials.web ?? {};

  if (!client_id || !client_secret) {
    throw new Error('Invalid credentials file — missing client_id or client_secret');
  }

  const oauth2Client = new google.auth.OAuth2(
    client_id,
    client_secret,
    redirect_uris?.[0] ?? 'http://localhost',
  );

  const tokenPath = expandPath(config.token_file ?? '~/.config/flynn/gmail-token.json');
  if (!existsSync(tokenPath)) {
    throw new Error(`Token file not found: ${tokenPath}. Run "flynn gmail-auth" to authenticate.`);
  }

  const token = JSON.parse(readFileSync(tokenPath, 'utf-8'));
  oauth2Client.setCredentials(token);

  return oauth2Client;
}

interface EmailSummary {
  id: string;
  from: string;
  subject: string;
  date: string;
  snippet: string;
}

/** Fetch metadata for a single message. */
async function fetchMessageDetails(
  gmail: ReturnType<typeof google.gmail>,
  messageId: string,
): Promise<EmailSummary | null> {
  try {
    const msg = await gmail.users.messages.get({
      userId: 'me',
      id: messageId,
      format: 'metadata',
      metadataHeaders: ['From', 'Subject', 'Date'],
    });

    const headers = msg.data.payload?.headers ?? [];
    const getHeader = (name: string): string =>
      headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value ?? '';

    return {
      id: messageId,
      from: getHeader('From'),
      subject: getHeader('Subject'),
      date: getHeader('Date'),
      snippet: sanitizeHtml(msg.data.snippet ?? ''),
    };
  } catch {
    return null;
  }
}

/** Decode a base64url-encoded string. */
function decodeBase64Url(data: string): string {
  return Buffer.from(data.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8');
}

/** Extract plain text body from a message payload, walking MIME parts recursively. */
function extractTextBody(payload: {
  mimeType?: string | null;
  body?: { data?: string | null } | null;
  parts?: Array<{
    mimeType?: string | null;
    body?: { data?: string | null } | null;
    parts?: unknown[];
  }> | null;
}): string {
  // Direct body (simple messages)
  if (payload.mimeType === 'text/plain' && payload.body?.data) {
    return decodeBase64Url(payload.body.data);
  }

  // Walk parts for multipart messages — prefer text/plain over text/html
  if (payload.parts) {
    let htmlFallback = '';
    for (const part of payload.parts) {
      if (part.mimeType === 'text/plain' && part.body?.data) {
        return decodeBase64Url(part.body.data);
      }
      if (part.mimeType === 'text/html' && part.body?.data) {
        htmlFallback = decodeBase64Url(part.body.data);
      }
      // Recurse into nested multipart
      if (part.parts) {
        const nested = extractTextBody(part as typeof payload);
        if (nested) {return nested;}
      }
    }
    if (htmlFallback) {
      // Convert HTML to clean plain text
      return sanitizeHtml(htmlFallback);
    }
  }

  return '';
}

/** Format a list of email summaries for tool output. */
function formatEmails(emails: EmailSummary[]): string {
  if (emails.length === 0) {
    return 'No messages found.';
  }

  return emails
    .map(e => `[${e.id}] ${e.date}\n  From: ${e.from}\n  Subject: ${e.subject}\n  ${e.snippet}`)
    .join('\n\n');
}

/**
 * Creates Gmail query tools bound to the given GmailConfig.
 * Tools create their own OAuth2 client, independent of GmailWatcher.
 */
export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
  const gmailList: Tool = {
    name: 'gmail.list',
    description:
      'List recent emails from Gmail. Returns id, from, subject, date, and snippet for each message.',
    requiredSecretScopes: ['gmail'],
    inputSchema: {
      type: 'object',
      properties: {
        maxResults: {
          type: 'number',
          description: 'Maximum number of emails to return (default: 10)',
        },
        label: {
          type: 'string',
          description: 'Gmail label to filter by (default: INBOX)',
        },
      },
    },
    execute: async (rawArgs: unknown): Promise<ToolResult> => {
      const args = rawArgs as { maxResults?: number; label?: string };
      const maxResults = args.maxResults ?? 10;
      const label = args.label ?? 'INBOX';

      try {
        const auth = createOAuth2Client(config);
        const gmail = google.gmail({ version: 'v1', auth });

        const listResponse = await gmail.users.messages.list({
          userId: 'me',
          labelIds: [label],
          maxResults,
        });

        const messages = listResponse.data.messages ?? [];
        const emails: EmailSummary[] = [];

        for (const msg of messages) {
          if (!msg.id) {continue;}
          const details = await fetchMessageDetails(gmail, msg.id);
          if (details) {emails.push(details);}
        }

        return {
          success: true,
          output: formatEmails(emails),
        };
      } catch (error) {
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };

  const gmailSearch: Tool = {
    name: 'gmail.search',
    description:
      'Search Gmail using Gmail query syntax (e.g. "from:user@example.com", "is:unread", "subject:hello"). Returns id, from, subject, date, and snippet for each match.',
    requiredSecretScopes: ['gmail'],
    inputSchema: {
      type: 'object',
      properties: {
        query: {
          type: 'string',
          description: 'Gmail search query (same syntax as Gmail search bar)',
        },
        maxResults: {
          type: 'number',
          description: 'Maximum number of results to return (default: 10)',
        },
      },
      required: ['query'],
    },
    execute: async (rawArgs: unknown): Promise<ToolResult> => {
      const args = rawArgs as { query: string; maxResults?: number };
      const maxResults = args.maxResults ?? 10;

      try {
        const auth = createOAuth2Client(config);
        const gmail = google.gmail({ version: 'v1', auth });

        const listResponse = await gmail.users.messages.list({
          userId: 'me',
          q: args.query,
          maxResults,
        });

        const messages = listResponse.data.messages ?? [];
        const emails: EmailSummary[] = [];

        for (const msg of messages) {
          if (!msg.id) {continue;}
          const details = await fetchMessageDetails(gmail, msg.id);
          if (details) {emails.push(details);}
        }

        return {
          success: true,
          output: formatEmails(emails),
        };
      } catch (error) {
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };

  const gmailRead: Tool = {
    name: 'gmail.read',
    description:
      'Read the full content of a Gmail message by ID. Returns headers (from, to, subject, date) and the full text body. Use gmail.list or gmail.search first to get message IDs.',
    requiredSecretScopes: ['gmail'],
    inputSchema: {
      type: 'object',
      properties: {
        id: {
          type: 'string',
          description: 'The Gmail message ID (from gmail.list or gmail.search results)',
        },
      },
      required: ['id'],
    },
    execute: async (rawArgs: unknown): Promise<ToolResult> => {
      const args = rawArgs as { id: string };

      try {
        const auth = createOAuth2Client(config);
        const gmail = google.gmail({ version: 'v1', auth });

        const msg = await gmail.users.messages.get({
          userId: 'me',
          id: args.id,
          format: 'full',
        });

        const headers = msg.data.payload?.headers ?? [];
        const getHeader = (name: string): string =>
          headers.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value ?? '';

        const body = msg.data.payload ? extractTextBody(msg.data.payload) : '';

        const parts = [
          `From: ${getHeader('From')}`,
          `To: ${getHeader('To')}`,
          `Subject: ${getHeader('Subject')}`,
          `Date: ${getHeader('Date')}`,
          '',
          body || '(no text content)',
        ];

        return {
          success: true,
          output: parts.join('\n'),
        };
      } catch (error) {
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };

  return [gmailList, gmailSearch, gmailRead];
}
