import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
  Page,
  Database,
  Block,
  User,
  Comment,
  SearchResult,
  PaginatedResults,
  Filter,
  Sort,
  PageId,
  DatabaseId,
  BlockId,
  UserId,
  CommentId,
  PageProperty,
  DatabaseProperty,
} from '../types/index.js';

export interface NotionClientConfig {
  auth: string;
  baseURL?: string;
  notionVersion?: string;
  maxRetries?: number;
  retryDelay?: number;
}

interface CreatePageParams {
  parent: { database_id: DatabaseId } | { page_id: PageId };
  properties: Record<string, Partial<PageProperty>>;
  icon?: Page['icon'];
  cover?: Page['cover'];
  children?: Block[];
}

interface UpdatePageParams {
  properties?: Record<string, Partial<PageProperty>>;
  icon?: Page['icon'];
  cover?: Page['cover'];
  archived?: boolean;
}

interface QueryDatabaseParams {
  database_id: DatabaseId;
  filter?: Filter;
  sorts?: Sort[];
  start_cursor?: string;
  page_size?: number;
}

interface CreateDatabaseParams {
  parent: { page_id: PageId };
  title: Array<{ type: 'text'; text: { content: string } }>;
  properties: Record<string, DatabaseProperty>;
  icon?: Database['icon'];
  cover?: Database['cover'];
  is_inline?: boolean;
}

interface SearchParams {
  query?: string;
  filter?: { value: 'page' | 'database'; property: 'object' };
  sort?: { direction: 'ascending' | 'descending'; timestamp: 'last_edited_time' };
  start_cursor?: string;
  page_size?: number;
}

interface CreateCommentParams {
  parent: { page_id: PageId } | { block_id: BlockId };
  rich_text: Array<{ type: 'text'; text: { content: string } }>;
}

export class NotionClient {
  private client: AxiosInstance;
  private maxRetries: number;
  private retryDelay: number;
  private lastRequestTime: number = 0;
  private readonly rateLimitRequests = 3; // 3 requests per second
  private readonly rateLimitWindow = 1000; // 1 second in ms

  constructor(config: NotionClientConfig) {
    this.maxRetries = config.maxRetries ?? 3;
    this.retryDelay = config.retryDelay ?? 1000;

    this.client = axios.create({
      baseURL: config.baseURL ?? 'https://api.notion.com/v1',
      headers: {
        'Authorization': `Bearer ${config.auth}`,
        'Notion-Version': config.notionVersion ?? '2022-06-28',
        'Content-Type': 'application/json',
      },
      timeout: 30000,
    });

    // Add response interceptor for error handling
    this.client.interceptors.response.use(
      (response) => response,
      (error: AxiosError) => {
        if (error.response) {
          const status = error.response.status;
          const data = error.response.data as { message?: string; code?: string };
          throw new Error(
            `Notion API error (${status}): ${data.message || data.code || 'Unknown error'}`
          );
        }
        throw error;
      }
    );
  }

  // Rate limiting helper
  private async applyRateLimit(): Promise<void> {
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequestTime;
    const minInterval = this.rateLimitWindow / this.rateLimitRequests;

    if (timeSinceLastRequest < minInterval) {
      await new Promise((resolve) =>
        setTimeout(resolve, minInterval - timeSinceLastRequest)
      );
    }

    this.lastRequestTime = Date.now();
  }

  // Retry helper with exponential backoff
  private async retryWithBackoff<T>(
    fn: () => Promise<T>,
    retries: number = this.maxRetries
  ): Promise<T> {
    try {
      await this.applyRateLimit();
      return await fn();
    } catch (error) {
      if (retries === 0) throw error;

      const isRetryable =
        error instanceof Error &&
        (error.message.includes('429') ||
          error.message.includes('503') ||
          error.message.includes('timeout'));

      if (!isRetryable) throw error;

      const delay = this.retryDelay * Math.pow(2, this.maxRetries - retries);
      await new Promise((resolve) => setTimeout(resolve, delay));
      return this.retryWithBackoff(fn, retries - 1);
    }
  }

  // Pagination helper
  private async *paginate<T>(
    fn: (cursor?: string) => Promise<PaginatedResults<T>>
  ): AsyncGenerator<T, void, undefined> {
    let cursor: string | undefined;
    let hasMore = true;

    while (hasMore) {
      const result = await fn(cursor);
      for (const item of result.results) {
        yield item;
      }
      hasMore = result.has_more;
      cursor = result.next_cursor ?? undefined;
    }
  }

  // Page methods
  async getPage(pageId: PageId): Promise<Page> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<Page>(`/pages/${pageId}`);
      return response.data;
    });
  }

  async createPage(params: CreatePageParams): Promise<Page> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.post<Page>('/pages', params);
      return response.data;
    });
  }

  async updatePage(pageId: PageId, params: UpdatePageParams): Promise<Page> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.patch<Page>(`/pages/${pageId}`, params);
      return response.data;
    });
  }

  // Database methods
  async getDatabase(databaseId: DatabaseId): Promise<Database> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<Database>(`/databases/${databaseId}`);
      return response.data;
    });
  }

  async createDatabase(params: CreateDatabaseParams): Promise<Database> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.post<Database>('/databases', params);
      return response.data;
    });
  }

  async updateDatabase(
    databaseId: DatabaseId,
    params: Partial<CreateDatabaseParams>
  ): Promise<Database> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.patch<Database>(
        `/databases/${databaseId}`,
        params
      );
      return response.data;
    });
  }

  async queryDatabase(params: QueryDatabaseParams): Promise<PaginatedResults<Page>> {
    return this.retryWithBackoff(async () => {
      const { database_id, ...body } = params;
      const response = await this.client.post<PaginatedResults<Page>>(
        `/databases/${database_id}/query`,
        body
      );
      return response.data;
    });
  }

  async *queryDatabaseAll(params: QueryDatabaseParams): AsyncGenerator<Page> {
    yield* this.paginate((cursor) =>
      this.queryDatabase({ ...params, start_cursor: cursor })
    );
  }

  // Block methods
  async getBlock(blockId: BlockId): Promise<Block> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<Block>(`/blocks/${blockId}`);
      return response.data;
    });
  }

  async getBlockChildren(
    blockId: BlockId,
    start_cursor?: string,
    page_size?: number
  ): Promise<PaginatedResults<Block>> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<PaginatedResults<Block>>(
        `/blocks/${blockId}/children`,
        {
          params: { start_cursor, page_size },
        }
      );
      return response.data;
    });
  }

  async *getBlockChildrenAll(blockId: BlockId): AsyncGenerator<Block> {
    yield* this.paginate((cursor) => this.getBlockChildren(blockId, cursor));
  }

  async appendBlockChildren(
    blockId: BlockId,
    children: Block[]
  ): Promise<PaginatedResults<Block>> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.patch<PaginatedResults<Block>>(
        `/blocks/${blockId}/children`,
        { children }
      );
      return response.data;
    });
  }

  async updateBlock(blockId: BlockId, block: Partial<Block>): Promise<Block> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.patch<Block>(`/blocks/${blockId}`, block);
      return response.data;
    });
  }

  async deleteBlock(blockId: BlockId): Promise<Block> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.delete<Block>(`/blocks/${blockId}`);
      return response.data;
    });
  }

  // User methods
  async getUser(userId: UserId): Promise<User> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<User>(`/users/${userId}`);
      return response.data;
    });
  }

  async listUsers(
    start_cursor?: string,
    page_size?: number
  ): Promise<PaginatedResults<User>> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<PaginatedResults<User>>('/users', {
        params: { start_cursor, page_size },
      });
      return response.data;
    });
  }

  async *listUsersAll(): AsyncGenerator<User> {
    yield* this.paginate((cursor) => this.listUsers(cursor));
  }

  async getBotUser(): Promise<User> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<User>('/users/me');
      return response.data;
    });
  }

  // Comment methods
  async createComment(params: CreateCommentParams): Promise<Comment> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.post<Comment>('/comments', params);
      return response.data;
    });
  }

  async listComments(
    blockId: BlockId,
    start_cursor?: string,
    page_size?: number
  ): Promise<PaginatedResults<Comment>> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.get<PaginatedResults<Comment>>('/comments', {
        params: { block_id: blockId, start_cursor, page_size },
      });
      return response.data;
    });
  }

  async *listCommentsAll(blockId: BlockId): AsyncGenerator<Comment> {
    yield* this.paginate((cursor) => this.listComments(blockId, cursor));
  }

  // Search method
  async search(params: SearchParams = {}): Promise<PaginatedResults<SearchResult>> {
    return this.retryWithBackoff(async () => {
      const response = await this.client.post<PaginatedResults<SearchResult>>(
        '/search',
        params
      );
      return response.data;
    });
  }

  async *searchAll(params: SearchParams = {}): AsyncGenerator<SearchResult> {
    yield* this.paginate((cursor) => this.search({ ...params, start_cursor: cursor }));
  }
}
