import type { AxiosResponse } from 'axios'
import crypto from 'node:crypto'
import fs from 'node:fs'
import { execa } from 'execa'
import type { GitHubConfig, GitHubProjectFieldMapping } from '../types'
import type { RepoProvider, WorkTrackerProvider } from './contracts'
import { providerRequest } from './http'

const API_VERSION = '2026-03-10'
const GITHUB_CARD_ID_RE = /^GH-(\d+)$/
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000
const DEFAULT_MAX_ATTEMPTS = 3
const DEFAULT_RETRY_BASE_DELAY_MS = 250

interface GitHubIssueResponse {
  number: number
  html_url: string
  node_id: string
}

interface GitHubCommentResponse {
  body?: string | null
}

interface InstallationTokenResponse {
  token: string
  expires_at: string
}

interface GraphQlResponse<T> {
  data?: T
  errors?: Array<{ message: string }>
}

export class GitHubProvider implements WorkTrackerProvider, RepoProvider {
  readonly kind = 'github' as const
  readonly label = 'GitHub'

  private cachedInstallationToken: { token: string; expiresAt: number } | null = null

  constructor(
    private readonly config: GitHubConfig,
    private readonly projectRoot?: string,
  ) {}

  async createCard(title: string, description: string): Promise<string> {
    const response = await this.rest<GitHubIssueResponse>('POST', this.issuePath(), {
      title,
      body: description,
      labels: this.config.labels ?? [],
    })

    await this.addIssueToProject(response.node_id)
    return `GH-${response.number}`
  }

  async updateDescription(cardId: string, description: string): Promise<void> {
    const issueNumber = this.issueNumber(cardId)
    await this.rest('PATCH', this.issuePath(issueNumber), { body: description })
  }

  async addComment(cardId: string, text: string): Promise<void> {
    const issueNumber = this.issueNumber(cardId)
    await this.rest('POST', `${this.issuePath(issueNumber)}/comments`, { body: text })
  }

  async getLastComment(cardId: string): Promise<string | null> {
    const issueNumber = this.issueNumber(cardId)
    const comments = await this.fetchLastCommentPage(issueNumber)
    const last = comments[comments.length - 1]
    return last?.body ?? null
  }

  isValidCardId(cardId: string): boolean {
    return this.tryIssueNumber(cardId) != null
  }

  normalizeCardId(cardId: string): string {
    return `GH-${this.issueNumber(cardId)}`
  }

  cardUrl(cardId: string): string | null {
    const issueNumber = this.tryIssueNumber(cardId)
    if (issueNumber == null) return null
    return `https://github.com/${this.config.owner}/${this.config.repo}/issues/${issueNumber}`
  }

  async ensureRemoteBranch(branch: string): Promise<void> {
    if (!this.projectRoot) {
      throw new Error('GitHubProvider.ensureRemoteBranch: projectRoot não configurado')
    }

    const exists = await this.remoteBranchExists(branch)
    if (exists) return
    await execa('git', ['push', 'origin', branch], {
      cwd: this.projectRoot,
      reject: true,
    })
  }

  async createPullRequest(
    branch: string,
    title: string,
    body: string,
    draft = true,
  ): Promise<{ number: number; url: string }> {
    const response = await this.rest<{ number: number; html_url: string }>(
      'POST',
      `/repos/${this.config.owner}/${this.config.repo}/pulls`,
      {
        title,
        head: branch,
        base: this.config.base_branch,
        body,
        draft,
      },
    )
    return { number: response.number, url: response.html_url }
  }

  static parseIssueNumber(cardId: string): number | null {
    const trimmed = cardId.trim()
    const gh = trimmed.match(GITHUB_CARD_ID_RE)
    if (gh) return Number(gh[1])

    const ownerRepo = trimmed.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#(\d+)$/)
    if (ownerRepo) return Number(ownerRepo[1])

    const url = trimmed.match(/^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/issues\/(\d+)(?:[?#].*)?$/)
    if (url) return Number(url[1])

    return null
  }

  private issuePath(issueNumber?: number): string {
    const base = `/repos/${this.config.owner}/${this.config.repo}/issues`
    return issueNumber == null ? base : `${base}/${issueNumber}`
  }

  private issueNumber(cardId: string): number {
    const issueNumber = this.tryIssueNumber(cardId)
    if (issueNumber == null) {
      throw new Error(`GitHubProvider: ID de issue inválido: ${cardId}`)
    }
    return issueNumber
  }

  private tryIssueNumber(cardId: string): number | null {
    return GitHubProvider.parseIssueNumber(cardId)
  }

  private async addIssueToProject(contentId: string): Promise<void> {
    const project = this.config.project
    if (!project || (!project.id && !project.number)) return

    const projectId = project.id ?? await this.fetchProjectId()
    if (!projectId) return

    const added = await this.graphql<{
      addProjectV2ItemById: { item: { id: string } }
    }>(
      `mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
        addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
          item { id }
        }
      }`,
      { projectId, contentId },
    )

    const itemId = added.addProjectV2ItemById.item.id
    await this.setDefaultProjectStatus(projectId, itemId, project.field_mapping)
  }

  private async fetchProjectId(): Promise<string | null> {
    const project = this.config.project
    if (!project?.number) return null

    const ownerType = project.owner_type ?? 'organization'
    const owner = project.owner ?? this.config.owner
    const query = ownerType === 'user'
      ? `query Project($login: String!, $number: Int!) {
          user(login: $login) { projectV2(number: $number) { id } }
        }`
      : `query Project($login: String!, $number: Int!) {
          organization(login: $login) { projectV2(number: $number) { id } }
        }`

    const result = await this.graphql<{
      user?: { projectV2?: { id: string } | null } | null
      organization?: { projectV2?: { id: string } | null } | null
    }>(query, { login: owner, number: project.number })

    return result.organization?.projectV2?.id ?? result.user?.projectV2?.id ?? null
  }

  private async setDefaultProjectStatus(
    projectId: string,
    itemId: string,
    mapping?: GitHubProjectFieldMapping,
  ): Promise<void> {
    const fieldId = mapping?.status_field_id
    const optionId = mapping?.default_status_option_id ?? mapping?.todo_option_id
    if (!fieldId || !optionId) return

    await this.graphql(
      `mutation SetProjectStatus($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
        updateProjectV2ItemFieldValue(input: {
          projectId: $projectId,
          itemId: $itemId,
          fieldId: $fieldId,
          value: { singleSelectOptionId: $optionId }
        }) {
          projectV2Item { id }
        }
      }`,
      { projectId, itemId, fieldId, optionId },
    )
  }

  private async remoteBranchExists(branch: string): Promise<boolean> {
    if (!this.projectRoot) return false
    const result = await execa('git', ['ls-remote', '--heads', 'origin', branch], {
      cwd: this.projectRoot,
      reject: true,
    })
    return result.stdout.trim().length > 0
  }

  private async rest<T = unknown>(method: string, path: string, data?: unknown): Promise<T> {
    const response = await this.restResponse<T>(method, path, data)
    return response.data
  }

  private async restResponse<T = unknown>(
    method: string,
    path: string,
    data?: unknown,
  ): Promise<AxiosResponse<T>> {
    const headers = await this.headers()
    return providerRequest<T>({
      operation: `GitHubProvider.${method} ${path}`,
      method,
      baseURL: 'https://api.github.com',
      url: path,
      data,
      headers,
      timeoutMs: this.requestTimeoutMs(),
      maxAttempts: this.maxAttempts(),
      retryDelayMs: this.retryBaseDelayMs(),
      redact: [headers.Authorization],
    })
  }

  private async fetchLastCommentPage(issueNumber: number): Promise<GitHubCommentResponse[]> {
    const commentsPath = `${this.issuePath(issueNumber)}/comments?per_page=100`
    const firstPage = await this.restResponse<GitHubCommentResponse[]>('GET', commentsPath)
    const lastUrl = this.lastRelUrl(this.headerValue(firstPage, 'link'))
    if (!lastUrl) return firstPage.data

    const lastPage = await this.restResponse<GitHubCommentResponse[]>(
      'GET',
      this.githubApiPath(lastUrl),
    )
    return lastPage.data
  }

  private headerValue(response: AxiosResponse<unknown>, name: string): string | undefined {
    const headers = response.headers as Record<string, unknown> & { get?: (key: string) => unknown }
    const value = headers[name] ?? headers[name.toLowerCase()] ?? headers.get?.(name)
    return Array.isArray(value) ? value.join(',') : typeof value === 'string' ? value : undefined
  }

  private lastRelUrl(linkHeader: string | undefined): string | null {
    if (!linkHeader) return null

    for (const rawPart of linkHeader.split(',')) {
      const part = rawPart.trim()
      const hasLastRel = part
        .split(';')
        .some((segment) => segment.trim() === 'rel="last"' || segment.trim() === 'rel=last')
      if (!hasLastRel) continue

      const match = part.match(/<([^>]+)>/)
      if (match?.[1]) return match[1]
    }

    return null
  }

  private githubApiPath(url: string): string {
    const parsed = new URL(url, 'https://api.github.com')
    if (parsed.origin !== 'https://api.github.com') return url
    return `${parsed.pathname}${parsed.search}`
  }

  private async graphql<T>(query: string, variables: Record<string, unknown>): Promise<T> {
    const response = await this.rest<GraphQlResponse<T>>('POST', '/graphql', {
      query,
      variables,
    })

    if (response.errors?.length) {
      throw new Error(response.errors.map((e) => e.message).join(' | '))
    }
    if (!response.data) {
      throw new Error('GitHub GraphQL não retornou data')
    }
    return response.data
  }

  private async headers(): Promise<Record<string, string>> {
    return {
      Authorization: `Bearer ${await this.token()}`,
      Accept: 'application/vnd.github+json',
      'X-GitHub-Api-Version': API_VERSION,
      'Content-Type': 'application/json',
    }
  }

  private async token(): Promise<string> {
    const auth = this.config.auth ?? {}
    if (auth.token) return auth.token

    const tokenEnv = auth.token_env ?? 'GITHUB_TOKEN'
    const envToken = process.env[tokenEnv] ?? process.env.GH_TOKEN
    if (envToken) return envToken

    if (auth.app_id && auth.installation_id && (auth.private_key || auth.private_key_env || auth.private_key_path)) {
      return this.installationToken()
    }

    throw new Error(
      `token GitHub ausente: defina github.auth.token_env (${tokenEnv}) ou configure GitHub App`,
    )
  }

  private async installationToken(): Promise<string> {
    const nowMs = Date.now()
    if (this.cachedInstallationToken && this.cachedInstallationToken.expiresAt - 60_000 > nowMs) {
      return this.cachedInstallationToken.token
    }

    const auth = this.config.auth ?? {}
    const jwt = this.githubAppJwt()
    const authHeader = `Bearer ${jwt}`
    const response = await providerRequest<InstallationTokenResponse>({
      operation: 'GitHubProvider.POST /app/installations/:installation_id/access_tokens',
      method: 'POST',
      url: `https://api.github.com/app/installations/${auth.installation_id}/access_tokens`,
      data: {},
      timeoutMs: this.requestTimeoutMs(),
      maxAttempts: this.maxAttempts(),
      retryDelayMs: this.retryBaseDelayMs(),
      headers: {
        Authorization: authHeader,
        Accept: 'application/vnd.github+json',
        'X-GitHub-Api-Version': API_VERSION,
      },
      redact: [authHeader],
    })

    this.cachedInstallationToken = {
      token: response.data.token,
      expiresAt: Date.parse(response.data.expires_at),
    }
    return response.data.token
  }

  private githubAppJwt(): string {
    const auth = this.config.auth ?? {}
    const privateKey = this.privateKey()
    const now = Math.floor(Date.now() / 1000)
    const header = this.base64Url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }))
    const payload = this.base64Url(JSON.stringify({
      iat: now - 60,
      exp: now + 540,
      iss: auth.app_id,
    }))
    const unsigned = `${header}.${payload}`
    const signature = crypto
      .createSign('RSA-SHA256')
      .update(unsigned)
      .sign(privateKey)
    return `${unsigned}.${this.base64Url(signature)}`
  }

  private privateKey(): string {
    const auth = this.config.auth ?? {}
    const raw = auth.private_key
      ?? (auth.private_key_env ? process.env[auth.private_key_env] : undefined)
      ?? (auth.private_key_path ? fs.readFileSync(auth.private_key_path, 'utf8') : undefined)

    if (!raw) {
      throw new Error('GitHub App private key ausente')
    }
    return raw.replace(/\\n/g, '\n')
  }

  private base64Url(value: string | Buffer): string {
    return Buffer.from(value)
      .toString('base64')
      .replace(/=/g, '')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
  }

  private requestTimeoutMs(): number {
    return this.positiveNumber(this.config.request_timeout_ms, DEFAULT_REQUEST_TIMEOUT_MS)
  }

  private maxAttempts(): number {
    return Math.max(1, Math.floor(this.positiveNumber(
      this.config.max_attempts,
      DEFAULT_MAX_ATTEMPTS,
    )))
  }

  private retryBaseDelayMs(): number {
    return this.positiveNumber(this.config.retry_base_delay_ms, DEFAULT_RETRY_BASE_DELAY_MS)
  }

  private positiveNumber(value: number | undefined, fallback: number): number {
    return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
  }
}
