import { Type } from '@sinclair/typebox'

import { getSocketDebug } from '@socketsecurity/lib/env/socket'
import { errorMessage } from '@socketsecurity/lib/errors'
import { envAsBoolean } from '@socketsecurity/lib-stable/env/boolean'
import { httpRequest } from '@socketsecurity/lib/http-request/request'

import { deduplicateArtifacts } from './artifacts.ts'
import { getSocketApiUrl } from './env.ts'
import { buildSocketHeaders } from './http-helpers.ts'
import { logger } from './logger.ts'
import { buildPurl } from './purl.ts'
import { AUTH_REQUIRED_MSG, errorResult, resolveAuthToken } from './server.ts'
import type { ToolErrorResult, ToolOkResult } from './server.ts'
import { buildSocketReportUrl } from './socket-url.ts'
import type { ToolSpec } from './tool-types.ts'

export interface DepscorePackageInput {
  ecosystem?: string | undefined
  depname: string
  version?: string | undefined
}

// Default Socket API URL. SOCKET_DEBUG=true points at localhost for local
// stack development; the default targets production. Both env vars resolved
// via fleet-canonical helpers.
const DEFAULT_SOCKET_API_URL = envAsBoolean(getSocketDebug())
  ? 'http://localhost:8866/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false'
  : 'https://api.socket.dev/v0/purl?alerts=false&compact=false&fixable=false&licenseattrib=false&licensedetails=false'

const SOCKET_API_URL = getSocketApiUrl() || DEFAULT_SOCKET_API_URL

// Single shared JSON Schema (via TypeBox) reused by both stdio and HTTP
// modes. TypeBox `Type.Object` returns native JSON Schema — the SDK ships
// it verbatim to clients as `Tool.inputSchema`.
const depscoreInputSchema = Type.Object({
  packages: Type.Array(
    Type.Object({
      ecosystem: Type.String({
        description:
          'Package ecosystem (PURL type): npm (JS/TS), pypi (Python), golang (Go), maven (Java/Scala/Kotlin), gem (Ruby), nuget (.NET), cargo (Rust), composer (PHP; "packagist" also accepted). See https://docs.socket.dev/docs/language-support',
        default: 'npm',
      }),
      depname: Type.String({ description: 'The name of the dependency' }),
      version: Type.String({
        description:
          "The version of the dependency, use 'unknown' if not known",
        default: 'unknown',
      }),
    }),
    { description: 'Array of packages to check' },
  ),
  platform: Type.Optional(
    Type.String({
      description:
        "Optional OS-architecture hint (e.g., 'linux-x64', 'darwin-arm64', 'win32-x64'). Used to select the most relevant artifact when a package has platform-specific builds.",
    }),
  ),
})

// Convert the depscore input list into PURLs ready for the components payload,
// stripping semver range prefixes from versions.
export function buildPackageComponents(
  packages: DepscorePackageInput[],
): Array<{ purl: string }> {
  return packages.map(pkg => {
    // Strip ^ and ~ range prefixes — depscore is a single-version lookup.
    const cleanedVersion = (pkg.version ?? 'unknown').replace(/[\^~]/g, '')
    const ecosystem = pkg.ecosystem ?? 'npm'
    const purl = buildPurl(ecosystem, pkg.depname, cleanedVersion)
    if (
      cleanedVersion !== '1.0.0' &&
      cleanedVersion !== 'unknown' &&
      cleanedVersion
    ) {
      logger.info(`Using version ${cleanedVersion} for ${pkg.depname}`)
    }
    return { purl }
  })
}

export function defineDepscoreTool(): ToolSpec {
  return {
    name: 'depscore',
    title: 'Dependency Score Tool',
    description:
      "Get the dependency score of packages with the `depscore` tool from Socket. Use 'unknown' for version if not known. Use this tool to scan dependencies for their quality and security on existing code or when code is generated. Stop generating code and ask the user how to proceed when any of the scores are low. When checking dependencies, make sure to also check the imports in the code, not just the manifest files (pyproject.toml, package.json, etc).",
    inputSchema: depscoreInputSchema,
    annotations: { readOnlyHint: true },
    handler(args, extra) {
      const packages = args['packages'] as DepscorePackageInput[]
      const platform = args['platform'] as string | undefined
      return handleDepscore(packages, platform, extra.authInfo?.token)
    },
  }
}

// Render `Object.entries(score)` into a human-readable "k1: v1, k2: v2"
// summary. Sub-1 floats render as percentages (0–100); values >1 render raw so
// non-percentage metrics aren't distorted.
export function formatScoreEntries(score: Record<string, unknown>): string {
  return Object.entries(score)
    .filter(([key]) => key !== 'overall' && key !== 'uuid')
    .map(([key, value]) => {
      const numValue = Number(value)
      if (!Number.isFinite(numValue)) {
        // A non-numeric score field would render the literal "NaN";
        // pass the raw value through instead.
        return `${key}: ${value}`
      }
      const displayValue = numValue <= 1 ? Math.round(numValue * 100) : numValue
      return `${key}: ${displayValue}`
    })
    .join(', ')
}

// Compose a "pkg:.../...@..." string + per-key score summary used by the
// depscore output. `score.overall` being defined gates whether we have real
// scores to render.
export function formatScoreLine(jsonData: Record<string, unknown>): string {
  const ns = jsonData['namespace'] ? `${jsonData['namespace']}/` : ''
  const purl = `pkg:${jsonData['type'] || 'unknown'}/${ns}${jsonData['name'] || 'unknown'}@${jsonData['version'] || 'unknown'}`
  const score = jsonData['score'] as Record<string, unknown> | undefined
  if (score && score['overall'] !== undefined) {
    const reportUrl = buildSocketReportUrl(jsonData)
    return `${purl}: ${formatScoreEntries(score)}\n  Report: ${reportUrl}`
  }
  return `${purl}: No score found`
}

export async function handleDepscore(
  packages: DepscorePackageInput[],
  platform: string | undefined,
  accessTokenFromAuth: string | undefined,
): Promise<ToolOkResult | ToolErrorResult> {
  logger.info(`Received request for ${packages.length} packages`)
  const accessToken = resolveAuthToken(accessTokenFromAuth)
  if (!accessToken) {
    logger.error(AUTH_REQUIRED_MSG)
    return errorResult(AUTH_REQUIRED_MSG)
  }

  const components = buildPackageComponents(packages)

  let response
  try {
    response = await httpRequest(SOCKET_API_URL, {
      method: 'POST',
      headers: buildSocketHeaders(accessToken),
      body: JSON.stringify({ components }),
    })
  } catch (e) {
    logger.error(`Error processing packages: ${errorMessage(e)}`)
    return errorResult('Error connecting to Socket API')
  }

  const responseText = response.text()

  if (response.status === 401) {
    const errorMsg = `Socket authentication failed [401]. Re-authenticate and retry. ${responseText}`
    logger.error(errorMsg)
    return errorResult(errorMsg)
  }

  if (response.status === 403) {
    const errorMsg = `Socket denied access [403]. Re-authenticate with the correct organization or repository permissions and retry. ${responseText}`
    logger.error(errorMsg)
    return errorResult(errorMsg)
  }

  if (response.status !== 200) {
    const errorMsg = `Error processing packages: [${response.status}] ${responseText}`
    logger.error(errorMsg)
    return errorResult(errorMsg)
  }

  if (!responseText.trim()) {
    const errorMsg = 'No packages were found.'
    logger.error(errorMsg)
    return errorResult(errorMsg)
  }

  try {
    const contentType = response.headers['content-type']
    const contentTypeValue = Array.isArray(contentType)
      ? contentType.join(',')
      : contentType || ''
    const isNdjson = contentTypeValue.includes('x-ndjson')

    const parseResult = isNdjson
      ? parseNdjsonPackageBody(responseText, platform)
      : parseSinglePackageBody(responseText)

    if (!Array.isArray(parseResult)) {
      return errorResult(parseResult.error)
    }

    return {
      content: [
        {
          type: 'text',
          text:
            parseResult.length > 0
              ? `Dependency scores:\n${parseResult.join('\n')}`
              : 'No scores found for the provided packages',
        },
      ],
    }
  } catch (e) {
    const errorMsg = `JSON parsing error: ${errorMessage(e)} -- Response: ${responseText}`
    logger.error(errorMsg)
    return errorResult('Error parsing response from Socket API')
  }
}

// Parse an NDJSON response body — one JSON document per line — into result
// lines, dropping `_type`-tagged control frames and running the platform-aware
// artifact deduplication before formatting.
export function parseNdjsonPackageBody(
  responseText: string,
  platform: string | undefined,
): string[] | { error: string } {
  // Parse line-by-line so one malformed line doesn't discard the whole
  // batch — NDJSON is line-oriented, and a truncated/garbage line is skipped
  // (logged) rather than thrown.
  const jsonLines: Array<Record<string, unknown>> = []
  const lines = responseText.split('\n')
  for (let i = 0, { length } = lines; i < length; i += 1) {
    const line = lines[i]!.trim()
    if (!line) {
      continue
    }
    let obj: Record<string, unknown>
    try {
      obj = JSON.parse(line) as Record<string, unknown>
    } catch (e) {
      logger.error(
        `Skipping malformed NDJSON line ${i + 1}: ${errorMessage(e)}`,
      )
      continue
    }
    if (!obj['_type']) {
      jsonLines.push(obj)
    }
  }

  if (!jsonLines.length) {
    return { error: 'No valid JSON objects found in NDJSON response' }
  }

  const deduplicated = deduplicateArtifacts(jsonLines, platform)
  const results: string[] = []
  for (let i = 0, { length } = deduplicated; i < length; i += 1) {
    results.push(formatScoreLine(deduplicated[i]!))
  }
  return results
}

// Parse a non-NDJSON response (single JSON document) into one result line.
export function parseSinglePackageBody(responseText: string): string[] {
  const jsonData = JSON.parse(responseText) as Record<string, unknown>
  return [formatScoreLine(jsonData)]
}
