import { application } from '@application'
import { loggerService } from '@logger'
import { net, safeStorage } from 'electron'
import fs from 'fs'
import path from 'path'

const logger = loggerService.withContext('CopilotService')

// 配置常量，集中管理
const CONFIG = {
  GITHUB_CLIENT_ID: 'Iv1.b507a08c87ecfe98',
  POLLING: {
    MAX_ATTEMPTS: 8,
    INITIAL_DELAY_MS: 1000,
    MAX_DELAY_MS: 16000 // 最大延迟16秒
  },
  DEFAULT_HEADERS: {
    accept: 'application/json',
    'editor-version': 'Neovim/0.6.1',
    'editor-plugin-version': 'copilot.vim/1.16.0',
    'content-type': 'application/json',
    'user-agent': 'GithubCopilot/1.155.0',
    'accept-encoding': 'gzip,deflate,br'
  },
  // API端点集中管理
  API_URLS: {
    GITHUB_USER: 'https://api.github.com/user',
    GITHUB_DEVICE_CODE: 'https://github.com/login/device/code',
    GITHUB_ACCESS_TOKEN: 'https://github.com/login/oauth/access_token',
    COPILOT_TOKEN: 'https://api.github.com/copilot_internal/v2/token'
  },
  TOKEN_FILE_NAME: '.copilot_token'
}

// 接口定义移到顶部，便于查阅
interface UserResponse {
  login: string
  avatar: string
}

interface AuthResponse {
  device_code: string
  user_code: string
  verification_uri: string
}

interface TokenResponse {
  access_token: string
}

interface CopilotTokenResponse {
  token: string
}

// 自定义错误类，统一错误处理
class CopilotServiceError extends Error {
  constructor(
    message: string,
    public readonly cause?: unknown
  ) {
    super(message)
    this.name = 'CopilotServiceError'
  }
}

class CopilotService {
  // Memoized backing field for the lazy `tokenFilePath` getter below.
  // `undefined` until first access; resolved exactly once and cached.
  private _tokenFilePath: string | undefined
  private headers: Record<string, string>

  constructor() {
    this.headers = {
      ...CONFIG.DEFAULT_HEADERS,
      accept: 'application/json',
      'user-agent': 'Visual Studio Code (desktop)'
    }
  }

  // TODO(v2): Lazy + memoized getter is a workaround, not a fix.
  //
  // The real problem is that `CopilotService` is exported as a top-level
  // singleton at the bottom of this file
  // (`export const copilotService = new CopilotService()`). That
  // singleton is instantiated during the static import graph of
  // `src/main/index.ts` (via `ipc.ts`), BEFORE
  // `application.bootstrap()` runs and builds the path registry. The
  // previous shape resolved `tokenFilePath` in the constructor
  // (`this.tokenFilePath = this.getTokenFilePath()`), which called
  // `application.getPath(...)` at instantiation time and threw
  // "PATHS not initialized".
  //
  // Lazy + cached resolution defers the path lookup until first *access*
  // (cached because `getTokenFilePath` does an `fs.existsSync` syscall
  // for the legacy-path fallback — we don't want that on every read).
  // But the class itself is still being constructed too early. We've
  // merely moved the path lookup out of construction; we have NOT
  // solved the architectural issue.
  //
  // The proper v2 fix is to migrate `CopilotService` into the lifecycle
  // system: extend `BaseService`, add `@Injectable`, register in
  // `serviceRegistry.ts`, and have callers resolve it via
  // `application.get('CopilotService')` instead of importing the
  // singleton. Once that's done, the DI container will instantiate it
  // inside `application.bootstrap()` after the path registry is built,
  // and the constructor can resolve `tokenFilePath` directly again.
  // Until then, keep this lazy getter — do NOT move the assignment
  // back to the constructor.
  private get tokenFilePath(): string {
    return (this._tokenFilePath ??= this.getTokenFilePath())
  }

  private getTokenFilePath = (): string => {
    // Legacy path: token was previously stored directly under userData
    const oldTokenFilePath = path.join(application.getPath('app.userdata'), CONFIG.TOKEN_FILE_NAME)
    if (fs.existsSync(oldTokenFilePath)) {
      return oldTokenFilePath
    }
    return application.getPath('feature.copilot.token_file')
  }

  /**
   * 设置自定义请求头
   */
  private updateHeaders = (headers?: Record<string, string>): void => {
    if (headers && Object.keys(headers).length > 0) {
      this.headers = { ...headers }
    }
  }

  /**
   * 获取GitHub登录信息
   */
  public getUser = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<UserResponse> => {
    try {
      const response = await net.fetch(CONFIG.API_URLS.GITHUB_USER, {
        method: 'GET',
        headers: {
          Connection: 'keep-alive',
          'user-agent': 'Visual Studio Code (desktop)',
          'Sec-Fetch-Site': 'none',
          'Sec-Fetch-Mode': 'no-cors',
          'Sec-Fetch-Dest': 'empty',
          accept: 'application/json',
          authorization: `token ${token}`
        }
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const data = await response.json()
      return {
        login: data.login,
        avatar: data.avatar_url
      }
    } catch (error) {
      logger.error('Failed to get user information:', error as Error)
      throw new CopilotServiceError('无法获取GitHub用户信息', error)
    }
  }

  /**
   * 获取GitHub设备授权信息
   */
  public getAuthMessage = async (
    _: Electron.IpcMainInvokeEvent,
    headers?: Record<string, string>
  ): Promise<AuthResponse> => {
    try {
      this.updateHeaders(headers)

      const response = await net.fetch(CONFIG.API_URLS.GITHUB_DEVICE_CODE, {
        method: 'POST',
        headers: {
          ...this.headers,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          client_id: CONFIG.GITHUB_CLIENT_ID,
          scope: 'read:user'
        })
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      return (await response.json()) as AuthResponse
    } catch (error) {
      logger.error('Failed to get auth message:', error as Error)
      throw new CopilotServiceError('无法获取GitHub授权信息', error)
    }
  }

  /**
   * 使用设备码获取访问令牌 - 优化轮询逻辑
   */
  public getCopilotToken = async (
    _: Electron.IpcMainInvokeEvent,
    device_code: string,
    headers?: Record<string, string>
  ): Promise<TokenResponse> => {
    this.updateHeaders(headers)

    let currentDelay = CONFIG.POLLING.INITIAL_DELAY_MS

    for (let attempt = 0; attempt < CONFIG.POLLING.MAX_ATTEMPTS; attempt++) {
      await this.delay(currentDelay)

      try {
        const response = await net.fetch(CONFIG.API_URLS.GITHUB_ACCESS_TOKEN, {
          method: 'POST',
          headers: {
            ...this.headers,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            client_id: CONFIG.GITHUB_CLIENT_ID,
            device_code,
            grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
          })
        })

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }

        const data = (await response.json()) as TokenResponse
        const { access_token } = data
        if (access_token) {
          return { access_token }
        }
      } catch (error) {
        // 指数退避策略
        currentDelay = Math.min(currentDelay * 2, CONFIG.POLLING.MAX_DELAY_MS)

        // 仅在最后一次尝试失败时记录详细错误
        const isLastAttempt = attempt === CONFIG.POLLING.MAX_ATTEMPTS - 1
        if (isLastAttempt) {
          logger.error(`Token polling failed after ${CONFIG.POLLING.MAX_ATTEMPTS} attempts:`, error as Error)
        }
      }
    }

    throw new CopilotServiceError('获取访问令牌超时，请重试')
  }

  /**
   * 保存Copilot令牌到本地文件
   */
  public saveCopilotToken = async (_: Electron.IpcMainInvokeEvent, token: string): Promise<void> => {
    try {
      const encryptedToken = safeStorage.encryptString(token)
      // 确保目录存在
      const dir = path.dirname(this.tokenFilePath)
      if (!fs.existsSync(dir)) {
        await fs.promises.mkdir(dir, { recursive: true })
      }

      await fs.promises.writeFile(this.tokenFilePath, encryptedToken)
    } catch (error) {
      logger.error('Failed to save token:', error as Error)
      throw new CopilotServiceError('无法保存访问令牌', error)
    }
  }

  /**
   * 从本地文件读取令牌并获取Copilot令牌
   */
  public getToken = async (
    _: Electron.IpcMainInvokeEvent,
    headers?: Record<string, string>
  ): Promise<CopilotTokenResponse> => {
    try {
      this.updateHeaders(headers)

      const encryptedToken = await fs.promises.readFile(this.tokenFilePath)
      const access_token = safeStorage.decryptString(Buffer.from(encryptedToken))

      const response = await net.fetch(CONFIG.API_URLS.COPILOT_TOKEN, {
        method: 'GET',
        headers: {
          ...this.headers,
          authorization: `token ${access_token}`
        }
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      return (await response.json()) as CopilotTokenResponse
    } catch (error) {
      logger.error('Failed to get Copilot token:', error as Error)
      throw new CopilotServiceError('无法获取Copilot令牌，请重新授权', error)
    }
  }

  /**
   * 退出登录，删除本地token文件
   */
  public logout = async (): Promise<void> => {
    try {
      try {
        await fs.promises.access(this.tokenFilePath)
        await fs.promises.unlink(this.tokenFilePath)
        logger.debug('Successfully logged out from Copilot')
      } catch (error) {
        // 文件不存在不是错误，只是记录一下
        logger.debug('Token file not found, nothing to delete')
      }
    } catch (error) {
      logger.error('Failed to logout:', error as Error)
      throw new CopilotServiceError('无法完成退出登录操作', error)
    }
  }

  /**
   * 辅助方法：延迟执行
   */
  private delay = (ms: number): Promise<void> => {
    return new Promise((resolve) => setTimeout(resolve, ms))
  }
}

export const copilotService = new CopilotService()
