import { Router, type Request, type Response } from 'express';
import path from 'path';
import { GitSourceService } from '../services/GitSourceService';
import { FileSystemService } from '../services/FileSystemService';
import { DatabaseService } from '../services/DatabaseService';
import { checkPermission, requirePermission } from '../middleware/permissions';
import { invalidateNodeCaches } from '../helpers/cacheInvalidation';
import { triggerPostDeployScan } from '../helpers/policyGate';
import { isValidGitSourcePath, isValidStackName } from '../utils/validation';
import { sendGitSourceError, webhookPullStatus } from '../utils/gitSourceHttp';
import { sanitizeForLog } from '../utils/safeLog';

// Reasonable upper bounds so a caller cannot flood the service with huge
// payloads. Generous compared to anything a real Git provider emits.
const MAX_REPO_URL_LENGTH = 2048;
const MAX_BRANCH_LENGTH = 256;
const MAX_COMPOSE_PATH_LENGTH = 1024;
const MAX_ENV_PATH_LENGTH = 1024;
const MAX_TOKEN_LENGTH = 8192;

/** Router for listing git-source configuration: `GET /api/git-sources`. */
export const gitSourcesRouter = Router();

gitSourcesRouter.get('/', async (req: Request, res: Response): Promise<void> => {
  try {
    const all = GitSourceService.getInstance().list();
    // Filter to the subset of stacks the caller can read. Keeps scoped
    // Admiral roles from discovering git config for stacks outside their grant.
    const visible = all.filter(src => checkPermission(req, 'stack:read', 'stack', src.stack_name));
    res.json(visible);
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

/**
 * Router for per-stack git-source endpoints. Mount at `/api/stacks` so the
 * `/:stackName/git-source*` paths work alongside other stack-scoped routes
 * (such as the label-assignments router extracted in Phase 4A-1).
 */
export const stackGitSourceRouter = Router();

stackGitSourceRouter.get('/:stackName/git-source', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:read', 'stack', stackName)) return;
  try {
    const source = GitSourceService.getInstance().get(stackName);
    if (source) {
      res.json(source);
      return;
    }
    // No source row. A non-existent stack is a genuine 404, but an existing
    // stack with no Git source attached is a normal, non-error state. The
    // dashboard probes this endpoint for every stack, so returning 404 here
    // would paint a console error for every unlinked stack; answer 200 with
    // a discriminator instead and reserve 404 for the stack-not-found case.
    const stacks = await FileSystemService.getInstance(req.nodeId).getStacks();
    if (!stacks.includes(stackName)) {
      res.status(404).json({ error: 'Stack not found' });
      return;
    }
    res.json({ linked: false });
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.put('/:stackName/git-source', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    const {
      repo_url,
      branch,
      compose_path,
      sync_env,
      env_path,
      auth_type,
      token,
      auto_apply_on_webhook,
      auto_deploy_on_apply,
    } = req.body ?? {};

    if (typeof repo_url !== 'string' || !repo_url.trim()) {
      res.status(400).json({ error: 'repo_url is required' });
      return;
    }
    if (typeof branch !== 'string' || !branch.trim()) {
      res.status(400).json({ error: 'branch is required' });
      return;
    }
    if (typeof compose_path !== 'string' || !compose_path.trim()) {
      res.status(400).json({ error: 'compose_path is required' });
      return;
    }
    if (auth_type !== 'none' && auth_type !== 'token') {
      res.status(400).json({ error: 'auth_type must be "none" or "token"' });
      return;
    }
    if (auto_apply_on_webhook !== undefined && typeof auto_apply_on_webhook !== 'boolean') {
      res.status(400).json({ error: 'auto_apply_on_webhook must be a boolean' });
      return;
    }
    if (auto_deploy_on_apply !== undefined && typeof auto_deploy_on_apply !== 'boolean') {
      res.status(400).json({ error: 'auto_deploy_on_apply must be a boolean' });
      return;
    }
    if (!/^https:\/\//i.test(repo_url)) {
      res.status(400).json({ error: 'Only HTTPS repository URLs are supported' });
      return;
    }
    if (repo_url.length > MAX_REPO_URL_LENGTH) {
      res.status(400).json({ error: 'repo_url is too long' });
      return;
    }
    if (branch.length > MAX_BRANCH_LENGTH) {
      res.status(400).json({ error: 'branch is too long' });
      return;
    }
    if (compose_path.length > MAX_COMPOSE_PATH_LENGTH) {
      res.status(400).json({ error: 'compose_path is too long' });
      return;
    }
    if (typeof env_path === 'string' && env_path.length > MAX_ENV_PATH_LENGTH) {
      res.status(400).json({ error: 'env_path is too long' });
      return;
    }
    if (!isValidGitSourcePath(compose_path.trim())) {
      res.status(400).json({ error: 'compose_path must be a relative repository file path' });
      return;
    }
    if (typeof env_path === 'string' && env_path.trim() && !isValidGitSourcePath(env_path.trim())) {
      res.status(400).json({ error: 'env_path must be a relative repository file path' });
      return;
    }
    if (typeof token === 'string' && token.length > MAX_TOKEN_LENGTH) {
      res.status(400).json({ error: 'token is too long' });
      return;
    }
    const autoApplyOnWebhook = auto_apply_on_webhook === true;
    const autoDeployOnApply = auto_deploy_on_apply === true;
    if (autoDeployOnApply && !requirePermission(req, res, 'stack:deploy', 'stack', stackName)) return;

    // Confirm the stack actually exists on the active node. Without this guard
    // a caller could stash a git-source row for a name that does not exist
    // yet and have it auto-link when a stack with that name is later created.
    const stacks = await FileSystemService.getInstance(req.nodeId).getStacks();
    if (!stacks.includes(stackName)) {
      res.status(404).json({ error: 'Stack not found' });
      return;
    }

    const syncEnv = Boolean(sync_env);
    const resolvedEnvPath = syncEnv
      ? (typeof env_path === 'string' && env_path.trim()
        ? env_path
        : path.posix.join(path.posix.dirname(compose_path.replace(/\\/g, '/')) || '.', '.env'))
      : null;

    const source = await GitSourceService.getInstance().upsert({
      stackName,
      repoUrl: repo_url.trim(),
      branch: branch.trim(),
      composePath: compose_path.trim(),
      syncEnv,
      envPath: resolvedEnvPath,
      authType: auth_type,
      token: typeof token === 'string' ? token : undefined,
      autoApplyOnWebhook,
      autoDeployOnApply,
    });

    console.log(`[GitSource] Configured git source for ${stackName}`);
    res.json(source);
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.delete('/:stackName/git-source', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    GitSourceService.getInstance().delete(stackName);
    console.log(`[GitSource] Removed git source for ${stackName}`);
    res.json({ success: true });
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.post('/:stackName/git-source/pull', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    const result = await GitSourceService.getInstance().pull(stackName);
    res.json(result);
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.post('/:stackName/git-source/apply', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    const { commitSha, deploy } = req.body ?? {};
    if (typeof commitSha !== 'string' || !commitSha.trim()) {
      res.status(400).json({ error: 'commitSha is required' });
      return;
    }
    const source = DatabaseService.getInstance().getGitSource(stackName);
    const willDeploy = typeof deploy === 'boolean' ? deploy : source?.auto_deploy_on_apply === true;
    if (willDeploy && !requirePermission(req, res, 'stack:deploy', 'stack', stackName)) return;
    const result = await GitSourceService.getInstance().apply(
      stackName,
      commitSha.trim(),
      {
        deploy: typeof deploy === 'boolean' ? deploy : undefined,
        actor: req.user?.username ?? 'unknown',
        bypassPolicy: req.query.ignorePolicy === 'true' && req.user?.role === 'admin',
      },
    );
    invalidateNodeCaches(req.nodeId);
    const shortSha = commitSha.trim().slice(0, 7);
    if (result.deployed) {
      console.log('[GitSource] Applied commit %s to %s (deployed)', sanitizeForLog(shortSha), sanitizeForLog(stackName));
    } else if (result.deployError) {
      console.warn('[GitSource] Applied commit %s to %s, deploy failed: %s', sanitizeForLog(shortSha), sanitizeForLog(stackName), sanitizeForLog(result.deployError));
    } else {
      console.log('[GitSource] Applied commit %s to %s', sanitizeForLog(shortSha), sanitizeForLog(stackName));
    }
    res.json(result);
    if (result.deployed) {
      triggerPostDeployScan(stackName, req.nodeId).catch(err =>
        console.error(`[Security] Post-deploy scan failed for ${sanitizeForLog(stackName)}:`, err),
      );
    }
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.post('/:stackName/git-source/webhook-pull', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    const source = GitSourceService.getInstance().get(stackName);
    if (!source) {
      res.status(404).json({ error: 'No Git source configured for this stack', status: 'error' });
      return;
    }
    if (source.auto_apply_on_webhook && source.auto_deploy_on_apply && !requirePermission(req, res, 'stack:deploy', 'stack', stackName)) return;
    const result = await GitSourceService.getInstance().handleWebhookPull(stackName);
    // Map the outcome to a real HTTP status so a Git provider sees a 4xx on
    // failure instead of a 200 with an error body (which it would read as
    // "delivered fine, stop retrying").
    res.status(webhookPullStatus(result.status)).json(result);
  } catch (error) {
    sendGitSourceError(res, error);
  }
});

stackGitSourceRouter.post('/:stackName/git-source/dismiss-pending', async (req: Request, res: Response): Promise<void> => {
  const stackName = req.params.stackName as string;
  if (!isValidStackName(stackName)) {
    res.status(400).json({ error: 'Invalid stack name' });
    return;
  }
  if (!requirePermission(req, res, 'stack:edit', 'stack', stackName)) return;
  try {
    GitSourceService.getInstance().dismissPending(stackName);
    res.json({ success: true });
  } catch (error) {
    sendGitSourceError(res, error);
  }
});
