import * as Sentry from "@sentry/node"
import { Request, Response } from "express"
import type { Octokit } from "octokit"
import { userNickname } from "../auth0-mgmt.js"
import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
import { posthog } from "../analytics.js"
import { AuthorizedUserReq } from "../types.js"
import { registerRepositoryAndMember } from "./utils/github-repo-setup.js"
import { createNewPullRequest } from "../github/create-pr-from-diffcontents.js"
import {
  missingRequiredFields,
  validationFailure,
  unauthorized,
  githubInstallationNotFound,
  githubInstallationError,
  githubNotCollaborator,
  internalServerError,
} from "../exceptions/index.js"

/**
 * Required GitHub App Permissions:
 * - contents: write (for creating branches and files)
 * - workflows: write (for creating/updating workflow files in .github/workflows/)
 * - pull_requests: write (for creating pull requests)
 *
 * The GitHub App must be installed on the target repository.
 */

// Dependencies interface for easier testing
export interface SetupGithubActionsDependencies {
  userNickname: typeof userNickname
  getInstallationOctokitByOwner: typeof getInstallationOctokitByOwner
  isUserCollaborator: typeof isUserCollaborator
  githubApp: typeof githubApp
  registerRepositoryAndMember: typeof registerRepositoryAndMember
  posthog: typeof posthog
}

// Default dependencies
let dependencies: SetupGithubActionsDependencies = {
  userNickname,
  getInstallationOctokitByOwner,
  isUserCollaborator,
  githubApp,
  registerRepositoryAndMember,
  posthog,
}

// For testing - allow dependency injection
export function setSetupGithubActionsDependencies(deps: Partial<SetupGithubActionsDependencies>) {
  dependencies = { ...dependencies, ...deps }
}

export function resetSetupGithubActionsDependencies() {
  dependencies = {
    userNickname,
    getInstallationOctokitByOwner,
    isUserCollaborator,
    githubApp,
    registerRepositoryAndMember,
    posthog,
  }
}

/**
 * Check if workflow file already exists on base branch
 * Returns the file content if it exists, null otherwise
 */
async function checkWorkflowFileExists(
  installationOctokit: Octokit,
  owner: string,
  repo: string,
  baseBranch: string,
): Promise<{ content: string; sha: string } | null> {
  try {
    const response = await installationOctokit.rest.repos.getContent({
      owner,
      repo,
      path: ".github/workflows/codeflash.yaml",
      ref: baseBranch,
    })

    if ("content" in response.data && response.data.encoding === "base64") {
      const content = Buffer.from(response.data.content, "base64").toString("utf-8")
      return { content, sha: response.data.sha }
    }
    return null
  } catch (error: any) {
    if (error.status === 404) {
      return null // File doesn't exist
    }
    throw error
  }
}

/**
 * Generate unique branch name for the PR
 */
function generateBranchName(): string {
  const timestamp = Date.now()
  return `codeflash/setup-github-actions-${timestamp}`
}

/**
 * Create PR with workflow file using Contents API
 */
async function createPrWithWorkflowFile(
  installationOctokit: Octokit,
  owner: string,
  repo: string,
  baseBranch: string,
  workflowContent: string,
): Promise<
  | { pr_url: string; pr_number: number; already_exists?: false }
  | { pr_url: null; pr_number: null; already_exists: true }
> {
  const filePath = ".github/workflows/codeflash.yaml"
  const branchName = generateBranchName()

  console.log(
    `[setup-github-actions.ts:createPrWithWorkflowFile] Creating PR for ${owner}/${repo} on branch ${baseBranch}, branchName=${branchName}, filePath=${filePath}`,
  )

  // Step 1: Get base branch SHA, with fallback to default branch if base branch doesn't exist
  let resolvedBaseBranch = baseBranch
  let baseRef
  try {
    baseRef = await installationOctokit.rest.git.getRef({
      owner,
      repo,
      ref: `heads/${baseBranch}`,
    })
  } catch (error: any) {
    if (error.status === 404) {
      // Base branch doesn't exist on GitHub (likely a local-only branch)
      // Fall back to the repository's default branch
      console.log(
        `[setup-github-actions.ts:createPrWithWorkflowFile] Base branch ${baseBranch} not found on GitHub, fetching default branch for ${owner}/${repo}`,
      )

      const repoInfo = await installationOctokit.rest.repos.get({
        owner,
        repo,
      })
      resolvedBaseBranch = repoInfo.data.default_branch

      console.log(
        `[setup-github-actions.ts:createPrWithWorkflowFile] Using default branch ${resolvedBaseBranch} for ${owner}/${repo}`,
      )

      // Get the default branch reference
      baseRef = await installationOctokit.rest.git.getRef({
        owner,
        repo,
        ref: `heads/${resolvedBaseBranch}`,
      })
    } else {
      // Re-throw other errors
      throw error
    }
  }

  const baseSha = baseRef.data.object.sha

  console.log(
    `[setup-github-actions.ts:createPrWithWorkflowFile] Base branch SHA: ${baseSha} for ${owner}/${repo} (using branch: ${resolvedBaseBranch})`,
  )

  // Step 2: Check if workflow file already exists (idempotency)
  const existingFile = await checkWorkflowFileExists(
    installationOctokit,
    owner,
    repo,
    resolvedBaseBranch,
  )

  if (existingFile) {
    // Compare content
    if (existingFile.content === workflowContent) {
      console.log(
        `[setup-github-actions] Workflow file already exists with same content, skipping PR creation`,
      )
      // File exists with same content - return early
      // This is a valid case - the workflow is already set up
      return {
        pr_url: null,
        pr_number: null,
        already_exists: true,
      }
    } else {
      console.log(
        `[setup-github-actions] Workflow file exists but content differs, creating PR with update`,
      )
    }
  }

  // Step 3: Create branch
  await installationOctokit.rest.git.createRef({
    owner,
    repo,
    ref: `refs/heads/${branchName}`,
    sha: baseSha,
  })

  console.log(
    `[setup-github-actions.ts:createPrWithWorkflowFile] Created branch ${branchName} for ${owner}/${repo}`,
  )

  // Step 4: Create workflow file using Contents API
  // This requires 'workflows: write' permission, which we now have
  const commitMessage = "Add CodeFlash GitHub Actions workflow"

  try {
    await installationOctokit.rest.repos.createOrUpdateFileContents({
      owner,
      repo,
      path: filePath,
      message: commitMessage,
      content: Buffer.from(workflowContent, "utf-8").toString("base64"),
      branch: branchName,
    })

    console.log(
      `[setup-github-actions.ts:createPrWithWorkflowFile] Created workflow file on branch ${branchName} for ${owner}/${repo}`,
    )
  } catch (error: any) {
    // Clean up the branch we created if file creation fails
    try {
      await installationOctokit.rest.git.deleteRef({
        owner,
        repo,
        ref: `heads/${branchName}`,
      })
      console.log(
        `[setup-github-actions.ts:createPrWithWorkflowFile] Cleaned up branch ${branchName} after error for ${owner}/${repo}`,
      )
    } catch (cleanupError: any) {
      console.error(
        `[setup-github-actions.ts:createPrWithWorkflowFile] Failed to cleanup branch ${branchName} for ${owner}/${repo}:`,
        cleanupError,
      )
    }

    throw error
  }

  // Step 5: Create PR using existing helper
  const prTitle = "Add CodeFlash GitHub Actions workflow"
  const prBody = `This PR adds the CodeFlash GitHub Actions workflow to automatically find optimizations for new code in future pull requests.

The workflow will:

- Run on every pull request when Python code is modified
- Automatically attempt to optimize Python code
- Create optimization suggestions when improvements are found

**Setup Required:**

To use this workflow, you'll need to add the \`CODEFLASH_API_KEY\` secret to your repository:

1. Get your API key from [codeflash.ai](https://app.codeflash.ai/apikeys)
2. Go to your repository settings: **Settings → Secrets and variables → Actions**
3. Click **New repository secret**
4. Name: \`CODEFLASH_API_KEY\`
5. Value: Your CodeFlash API key
6. Click **Add secret**

The workflow will use this key to authenticate with CodeFlash's optimization service.`

  const prResponse = await createNewPullRequest(
    installationOctokit,
    owner,
    repo,
    prTitle,
    prBody,
    branchName,
    resolvedBaseBranch,
  )

  const prUrl = prResponse.data.html_url
  const prNumber = prResponse.data.number

  console.log(
    `[setup-github-actions.ts:createPrWithWorkflowFile] Created PR #${prNumber}: ${prUrl} for ${owner}/${repo}`,
  )

  return { pr_url: prUrl, pr_number: prNumber }
}

/**
 * Main endpoint handler for setting up GitHub Actions
 */
export async function setupGithubActions(req: Request, res: Response): Promise<void> {
  try {
    const { owner, repo, baseBranch, workflowContent } = req.body

    // Validate required fields
    if (!owner || !repo || !baseBranch || !workflowContent) {
      throw missingRequiredFields("owner, repo, baseBranch, workflowContent")
    }

    // Validate workflowContent is a string
    if (typeof workflowContent !== "string") {
      throw validationFailure("workflowContent must be a string")
    }

    console.log(
      `[setup-github-actions.ts:setupGithubActions] Received request with parameters: owner=${owner}, repo=${repo}, baseBranch=${baseBranch}, workflowContentLength=${workflowContent.length}`,
    )

    const userId = (req as any).userId

    // Get user nickname for authentication
    const nickname: string | null = await dependencies.userNickname(userId)
    if (nickname == null) {
      throw unauthorized("")
    }

    console.log(
      `[setup-github-actions.ts:setupGithubActions] Request from user ${nickname} for ${owner}/${repo}`,
    )

    // Get installation Octokit
    const installationOctokit = await dependencies.getInstallationOctokitByOwner(
      dependencies.githubApp,
      owner,
      repo,
      userId,
    )

    if (installationOctokit instanceof Error) {
      console.error(
        `[setup-github-actions.ts:setupGithubActions] Error getting installation Octokit for ${owner}/${repo}: ${installationOctokit.message}`,
      )

      // Check if it's a "not installed" error (404)
      if (installationOctokit.message.includes("not installed")) {
        throw githubInstallationNotFound(`${owner}/${repo}`)
      }

      // Other installation errors
      throw githubInstallationError(installationOctokit.message)
    }

    // Verify user is collaborator
    const isCollaborator = await dependencies.isUserCollaborator(
      installationOctokit,
      owner,
      repo,
      nickname,
    )

    if (!isCollaborator) {
      console.log(
        `[setup-github-actions.ts:setupGithubActions] ${nickname} is not a collaborator on ${owner}/${repo}`,
      )
      throw githubNotCollaborator(`${owner}/${repo}`)
    }

    console.log(
      `[setup-github-actions.ts:setupGithubActions] Verified ${nickname} is a collaborator on ${owner}/${repo}`,
    )

    // Register repository and member in background
    dependencies
      .registerRepositoryAndMember(owner, repo, nickname, userId, installationOctokit)
      .then(() => {
        console.log(
          `[setup-github-actions.ts:setupGithubActions] Background repo and member upsert completed for ${owner}/${repo}`,
        )
      })
      .catch(err => {
        console.error(
          `[setup-github-actions.ts:setupGithubActions] Error in background upsert for ${owner}/${repo}:`,
          err,
        )
        Sentry.captureException(err)
      })

    // Create PR with workflow file
    const result = await createPrWithWorkflowFile(
      installationOctokit,
      owner,
      repo,
      baseBranch,
      workflowContent,
    )

    // Handle case where file already exists with same content
    if (result.already_exists) {
      res.status(200).json({
        success: true,
        pr_url: null,
        pr_number: null,
        message: "Workflow file already exists with the same content. No changes needed.",
        already_exists: true,
      })
      return
    }

    // Track success
    dependencies.posthog?.capture({
      distinctId: `github|${userId}`,
      event: "cfapi-setup-github-actions-success",
      properties: {
        owner,
        repo,
        pr_number: result.pr_number,
      },
    })

    res.status(200).json({
      success: true,
      pr_url: result.pr_url,
      pr_number: result.pr_number,
      message: "GitHub Actions workflow PR created successfully",
    })
  } catch (error: any) {
    // Re-throw AppExceptions to be handled by GlobalExceptionHandler
    if (error && typeof error === "object" && "getHttpStatus" in error) {
      throw error
    }

    const { owner, repo, baseBranch } = req.body || {}
    console.error(
      `[setup-github-actions.ts:setupGithubActions] Error processing request for ${owner}/${repo} on branch ${baseBranch}:`,
      error,
    )
    Sentry.captureException(error)

    const userId = (req as any).userId
    dependencies.posthog?.capture({
      distinctId: `github|${userId}`,
      event: "cfapi-setup-github-actions-error",
      properties: {
        error: error.message || "Unknown error",
      },
    })

    // Check for specific error types and provide helpful messages
    if (error.status === 404 && error.message?.includes("not installed")) {
      throw githubInstallationNotFound(`${owner}/${repo}`)
    }

    if (error.status === 403 || error.message?.includes("not a collaborator")) {
      throw githubNotCollaborator(`${owner}/${repo}`)
    }

    if (error.status === 401 || error.message?.includes("Unauthorized")) {
      throw unauthorized(error.message || "Authentication failed")
    }

    // For other errors, throw internal server error
    throw internalServerError(error.message || "Failed to setup GitHub Actions")
  }
}
