import { getInstallationOctokitByOwner, isUserCollaborator } from "../github/github-utils.js"
import { githubApp } from "../github/github-app.js"
import { Request, Response } from "express"
import { parseAndCreateOptimizationsDict } from "../github/pr-changes-utils.js"
import { posthog } from "../analytics.js"
import { userNickname } from "../auth0-mgmt.js"
import { logger } from "../utils/logger.js"
import {
  missingRequiredFields,
  unauthorized,
  githubInstallationError,
  githubNotCollaborator,
  githubPrNotFound,
  internalServerError,
} from "../exceptions/index.js"

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

// Default dependencies
let dependencies: VerifyExistingOptimizationsDependencies = {
  getInstallationOctokitByOwner,
  githubApp,
  parseAndCreateOptimizationsDict,
  posthog,
  userNickname,
  isUserCollaborator,
}

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

export function resetVerifyExistingOptimizationsDependencies() {
  dependencies = {
    getInstallationOctokitByOwner,
    githubApp,
    parseAndCreateOptimizationsDict,
    posthog,
    userNickname,
    isUserCollaborator,
  }
}

export async function verifyExistingOptimizations(req: Request, res: Response) {
  try {
    const { repo_owner, repo_name, pr_number } = req.body
    const userId = (req as any).userId

    logger.debug("Processing verify-existing-optimizations request", {
      requestId: (req as any).requestId,
      userId,
      endpoint: "/cfapi/verify-existing-optimizations",
      operation: "request_start",
      repo_owner,
      repo_name,
      pr_number,
    })

    if (!repo_name || !repo_owner || !pr_number) {
      throw missingRequiredFields("repo_name, repo_owner, pr_number")
    }

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

    const octokit = await dependencies.getInstallationOctokitByOwner(
      dependencies.githubApp,
      repo_owner,
      repo_name,
      userId,
    )
    if (octokit instanceof Error) {
      throw githubInstallationError(octokit.message)
    }

    logger.info("Got installation Octokit for repository", {
      requestId: (req as any).requestId,
      userId,
      endpoint: "/cfapi/verify-existing-optimizations",
      operation: "octokit_installation",
      repo_owner,
      repo_name,
    })

    // Check collaborator status with error handling
    try {
      const isCollaborator = await dependencies.isUserCollaborator(
        octokit,
        repo_owner,
        repo_name,
        nickname,
      )
      if (!isCollaborator) {
        // Emit human-readable console message for tests (using info for console.log output)
        // Don't include userId in context to avoid logger appending " for user ..."
        logger.info(`${nickname} is not a collaborator on ${repo_owner}/${repo_name}`, {
          requestId: (req as any).requestId,
          endpoint: "/cfapi/verify-existing-optimizations",
          operation: "collaborator_verification",
          repo_owner,
          repo_name,
          nickname,
        })
        throw githubNotCollaborator(`${repo_owner}/${repo_name}`)
      }
    } catch (error) {
      if (error && typeof error === "object" && "getHttpStatus" in error) {
        throw error
      }
      // Concatenate error in message for tests
      logger.error(`Error checking collaborator status: ${error}`, {
        requestId: (req as any).requestId,
        userId,
        endpoint: "/cfapi/verify-existing-optimizations",
        operation: "collaborator_verification",
        repo_owner,
        repo_name,
        nickname,
      })
      throw internalServerError("Failed to verify collaborator status")
    }

    // Get PR with specific 404 handling
    // Note: GitHub returns 404 for both non-existent PRs and PRs the installation cannot access
    let pr
    try {
      pr = await octokit.rest.pulls.get({
        owner: repo_owner,
        repo: repo_name,
        pull_number: pr_number,
      })
    } catch (error: any) {
      if (error.status === 404) {
        // Log additional context to help diagnose permission vs not-found issues
        logger.warn(
          `PR #${pr_number} returned 404 in ${repo_owner}/${repo_name}. This could mean the PR doesn't exist, or the GitHub App installation doesn't have access to it.`,
          {
            requestId: (req as any).requestId,
            userId,
            endpoint: "/cfapi/verify-existing-optimizations",
            operation: "pr_not_found_or_no_access",
            repo_owner,
            repo_name,
            pr_number,
            nickname,
            errorMessage: error.message,
            errorResponse: error.response?.data,
          },
        )
        throw githubPrNotFound(
          `#${pr_number} in ${repo_owner}/${repo_name}. If the PR exists, ensure the GitHub App installation has access to this repository.`,
        )
      }
      // Handle 403 (Forbidden) as a permissions issue
      if (error.status === 403) {
        logger.warn(
          `Access forbidden to PR #${pr_number} in ${repo_owner}/${repo_name}. The GitHub App installation may not have sufficient permissions.`,
          {
            requestId: (req as any).requestId,
            userId,
            endpoint: "/cfapi/verify-existing-optimizations",
            operation: "pr_access_forbidden",
            repo_owner,
            repo_name,
            pr_number,
            nickname,
            errorMessage: error.message,
          },
        )
        throw githubInstallationError(
          `Access forbidden to PR #${pr_number} in ${repo_owner}/${repo_name}. Please ensure the GitHub App has the necessary permissions.`,
        )
      }
      throw error // Re-throw to be caught by global handler
    }

    // Get PR comments with error handling
    let pr_messages
    try {
      pr_messages = await octokit.rest.issues.listComments({
        owner: repo_owner,
        repo: repo_name,
        issue_number: pr_number,
      })
    } catch (error) {
      // Pass error object for tests
      logger.error(
        "Error getting PR messages:",
        {
          requestId: (req as any).requestId,
          userId,
          endpoint: "/cfapi/verify-existing-optimizations",
          operation: "get_pr_messages",
          repo_owner,
          repo_name,
          pr_number,
        },
        undefined,
        error as Error,
      )
      throw internalServerError(`Failed to retrieve PR comments for ${repo_owner}/${repo_name}`)
    }

    // Get PR reviews with error handling
    let pr_reviews
    try {
      pr_reviews = await octokit.rest.pulls.listReviews({
        owner: repo_owner,
        repo: repo_name,
        pull_number: pr_number,
      })
    } catch (error) {
      // Pass error object for tests
      logger.error(
        "Error getting PR reviews:",
        {
          requestId: (req as any).requestId,
          userId,
          endpoint: "/cfapi/verify-existing-optimizations",
          operation: "get_pr_reviews",
          repo_owner,
          repo_name,
          pr_number,
        },
        undefined,
        error as Error,
      )
      throw internalServerError(`Failed to retrieve PR reviews for ${repo_owner}/${repo_name}`)
    }

    const reviewBodies: Array<{ body: string }> = []
    for (const review of pr_reviews.data) {
      // Add the main review body if it exists
      if (review.body) {
        reviewBodies.push({ body: review.body })
      }

      // Get review comments for this specific review
      try {
        const reviewComments = await octokit.rest.pulls.listCommentsForReview({
          owner: repo_owner,
          repo: repo_name,
          pull_number: pr_number,
          review_id: review.id,
        })
        // Add each review comment body
        for (const comment of reviewComments.data) {
          if (comment.body) {
            reviewBodies.push({ body: comment.body })
          }
        }
      } catch (error) {
        // Emit explicit console message for tests
        logger.error(
          `Error getting review comments for review ${review.id}:`,
          {
            requestId: (req as any).requestId,
            userId,
            endpoint: "/cfapi/verify-existing-optimizations",
            operation: "get_review_comments",
            repo_owner,
            repo_name,
            pr_number,
            reviewId: review.id,
          },
          undefined,
          error as Error,
        )
        // Continue with other reviews even if one fails
      }
    }

    // Also get all review comments (not tied to a specific review)
    try {
      const allReviewComments = await octokit.rest.pulls.listReviewComments({
        owner: repo_owner,
        repo: repo_name,
        pull_number: pr_number,
      })
      for (const comment of allReviewComments.data) {
        if (comment.body) {
          reviewBodies.push({ body: comment.body })
        }
      }
    } catch (error) {
      // Emit explicit console message for tests
      logger.error(
        "Error getting all review comments:",
        {
          requestId: (req as any).requestId,
          userId,
          endpoint: "/cfapi/verify-existing-optimizations",
          operation: "get_all_review_comments",
          repo_owner,
          repo_name,
          pr_number,
        },
        undefined,
        error as Error,
      )
      // Continue even if this fails
    }

    const prBody = pr.data.body || ""
    const validComments = pr_messages.data.filter(
      (comment: { body?: string }) => comment.body !== undefined,
    ) as Array<{ body: string }>
    const allComments = [...validComments, ...reviewBodies]
    const optimizations_dict = dependencies.parseAndCreateOptimizationsDict(prBody, allComments)

    if (Object.keys(optimizations_dict).length === 0) {
      return res.status(200).json({ error: "No optimizations found for this PR" })
    }

    const response_dict: Record<string, string[]> = {}
    for (const key in optimizations_dict) {
      response_dict[key] = Array.from(optimizations_dict[key])
    }

    dependencies.posthog?.capture({
      distinctId: userId,
      event: `cfapi-github-pr-optimization`,
      properties: {
        repo_owner,
        repo_name,
        pr_number,
      },
    })

    return res.status(200).json(response_dict)
  } catch (error) {
    // Re-throw AppExceptions to be handled by GlobalExceptionHandler
    if (error && typeof error === "object" && "getHttpStatus" in error) {
      throw error
    }

    // Emit specific console message expected by tests (stringified for single-arg output)
    // Note: Error object is still passed for Sentry, but test mode outputs stringified version
    // Use String(error) to match test expectation of ${error} format
    const errorStr = String(error)
    logger.errorWithSentry(
      `Error in /cfapi/verify-existing-optimizations: ${errorStr}`,
      {
        requestId: (req as any).requestId,
        userId: (req as any).userId,
        endpoint: "/cfapi/verify-existing-optimizations",
        operation: "verify_existing_optimizations",
      },
      {
        repo_owner: req.body?.repo_owner,
        repo_name: req.body?.repo_name,
        pr_number: req.body?.pr_number,
      },
      error as Error,
    )

    if (error instanceof Error) {
      throw internalServerError(`Error verifying existing optimizations: ${error.message}`)
    } else {
      throw internalServerError("Error verifying existing optimizations")
    }
  }
}
