import express from 'express';
import { promises as fs } from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import os from 'os';
import {
  addProjectManually,
  extractProjectDirectory,
} from '../projects.js';
import {
  getProjectDiscoveryContext,
  getProjectDiscoveryPlansOverview,
  getProjectDiscoveryPlanReport,
  rerunDiscoveryPlan,
  getProjectWorkCycles,
  applyWorkCycle,
  archiveWorkCycle,
} from '../discovery-plans.js';

const router = express.Router();

function sanitizeGitError(message, token) {
  if (!message || !token) return message;
  return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***');
}

// Default root used by the folder browser when no path is provided.
export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();

// System-critical paths that should never be used as workspace directories
export const FORBIDDEN_PATHS = [
  // Unix
  '/',
  '/etc',
  '/bin',
  '/sbin',
  '/usr',
  '/dev',
  '/proc',
  '/sys',
  '/var',
  '/boot',
  '/root',
  '/lib',
  '/lib64',
  '/opt',
  '/run',
  // Windows
  'C:\\Windows',
  'C:\\Program Files',
  'C:\\Program Files (x86)',
  'C:\\ProgramData',
  'C:\\System Volume Information',
  'C:\\$Recycle.Bin'
];

function isForbiddenWorkspacePath(inputPath) {
  const normalizedPath = path.normalize(path.resolve(inputPath));
  if (normalizedPath === '/' || FORBIDDEN_PATHS.includes(normalizedPath)) {
    return true;
  }

  for (const forbidden of FORBIDDEN_PATHS) {
    if (normalizedPath === forbidden || normalizedPath.startsWith(forbidden + path.sep)) {
      // Exception: allow user-accessible temporary folders under /var.
      if (
        forbidden === '/var' &&
        (normalizedPath.startsWith('/var/tmp') || normalizedPath.startsWith('/var/folders'))
      ) {
        continue;
      }
      return true;
    }
  }

  return false;
}

/**
 * Validates that a path is safe for workspace operations
 * @param {string} requestedPath - The path to validate
 * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>}
 */
export async function validateWorkspacePath(requestedPath) {
  try {
    // Resolve to absolute path
    let absolutePath = path.resolve(requestedPath);

    // Reject system-critical directories and descendants.
    if (isForbiddenWorkspacePath(absolutePath)) {
      return {
        valid: false,
        error: 'Cannot create workspace in system-critical directories'
      };
    }

    // Try to resolve the real path (following symlinks)
    let realPath;
    try {
      // Check if path exists to resolve real path
      await fs.access(absolutePath);
      realPath = await fs.realpath(absolutePath);
    } catch (error) {
      if (error.code === 'ENOENT') {
        // Path doesn't exist yet - check parent directory
        let parentPath = path.dirname(absolutePath);
        try {
          const parentRealPath = await fs.realpath(parentPath);

          // Reconstruct the full path with real parent
          realPath = path.join(parentRealPath, path.basename(absolutePath));
        } catch (parentError) {
          if (parentError.code === 'ENOENT') {
            // Parent doesn't exist either - use the absolute path as-is
            // We'll validate it's within allowed root
            realPath = absolutePath;
          } else {
            throw parentError;
          }
        }
      } else {
        throw error;
      }
    }

    // Apply the same checks after symlink/canonical path resolution.
    if (isForbiddenWorkspacePath(realPath)) {
      return {
        valid: false,
        error: 'Resolved path points to a system-critical directory'
      };
    }

    // Additional symlink check for existing paths
    try {
      await fs.access(absolutePath);
      const stats = await fs.lstat(absolutePath);

      if (stats.isSymbolicLink()) {
        // Verify symlink target is not a forbidden system path.
        const linkTarget = await fs.readlink(absolutePath);
        const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
        const realTarget = await fs.realpath(resolvedTarget);

        if (isForbiddenWorkspacePath(realTarget)) {
          return {
            valid: false,
            error: 'Symlink target points to a system-critical directory'
          };
        }
      }
    } catch (error) {
      if (error.code !== 'ENOENT') {
        throw error;
      }
      // Path doesn't exist - that's fine for new workspace creation
    }

    return {
      valid: true,
      resolvedPath: realPath
    };

  } catch (error) {
    return {
      valid: false,
      error: `Path validation failed: ${error.message}`
    };
  }
}

function getTrimmedParam(value) {
  return typeof value === 'string' ? value.trim() : '';
}

function getDiscoveryPlanErrorMessage(error, fallback) {
  if (error instanceof Error && error.message.trim().length > 0) {
    return error.message;
  }
  return fallback;
}

function getDiscoveryPlanErrorStatus(error) {
  if (error?.code === 'NOT_FOUND') {
    return 404;
  }
  if (error?.code === 'UNSUPPORTED_STRATEGY') {
    return 400;
  }
  if (error?.code === 'INVALID_STATE' || error?.code === 'MISSING_PLAN_BODY' || error?.code === 'MISSING_WORKSPACE') {
    return 409;
  }
  if (error?.code === 'ALREADY_RUNNING') {
    return 409;
  }
  return 500;
}

export async function handleGetProjectDiscoveryPlans(req, res) {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    if (!projectName) {
      return res.status(400).json({ error: 'projectName is required' });
    }

    const overview = await getProjectDiscoveryPlansOverview(projectName);
    return res.json(overview);
  } catch (error) {
    return res.status(500).json({ error: error.message });
  }
}

export async function handleGetProjectDiscoveryContext(req, res) {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    if (!projectName) {
      return res.status(400).json({ error: 'projectName is required' });
    }

    const context = await getProjectDiscoveryContext(projectName);
    return res.json(context);
  } catch (error) {
    return res.status(500).json({ error: error.message });
  }
}

export async function handleExecuteProjectDiscoveryPlan(req, res) {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    const planId = getTrimmedParam(req.params?.planId);
    if (!projectName) {
      return res.status(400).json({ error: 'projectName is required' });
    }
    if (!planId) {
      return res.status(400).json({ error: 'planId is required' });
    }

    const result = await rerunDiscoveryPlan(projectName, planId);
    return res.json(result);
  } catch (error) {
    return res.status(getDiscoveryPlanErrorStatus(error)).json({
      error: getDiscoveryPlanErrorMessage(error, 'Failed to rerun discovery plan')
    });
  }
}

router.get('/:projectName/discovery-context', handleGetProjectDiscoveryContext);
router.get('/:projectName/discovery-plans', handleGetProjectDiscoveryPlans);
router.post('/:projectName/discovery-plans/:planId/execute', handleExecuteProjectDiscoveryPlan);

router.get('/:projectName/discovery-plans/:planId/report', async (req, res) => {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    const planId = getTrimmedParam(req.params?.planId);
    if (!projectName) return res.status(400).json({ error: 'projectName is required' });
    if (!planId) return res.status(400).json({ error: 'planId is required' });

    const result = await getProjectDiscoveryPlanReport(projectName, planId);
    return res.json(result);
  } catch (error) {
    return res.status(getDiscoveryPlanErrorStatus(error)).json({
      error: getDiscoveryPlanErrorMessage(error, 'Failed to read discovery plan report')
    });
  }
});

router.get('/:projectName/work-cycles', async (req, res) => {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    if (!projectName) return res.status(400).json({ error: 'projectName is required' });

    const result = await getProjectWorkCycles(projectName);
    return res.json(result);
  } catch (error) {
    return res.status(getDiscoveryPlanErrorStatus(error)).json({
      error: getDiscoveryPlanErrorMessage(error, 'Failed to get work cycles')
    });
  }
});

router.post('/:projectName/work-cycles/:cycleId/apply', async (req, res) => {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    const cycleId = getTrimmedParam(req.params?.cycleId);
    if (!projectName) return res.status(400).json({ error: 'projectName is required' });
    if (!cycleId) return res.status(400).json({ error: 'cycleId is required' });

    const result = await applyWorkCycle(projectName, cycleId);
    return res.json(result);
  } catch (error) {
    return res.status(getDiscoveryPlanErrorStatus(error)).json({
      error: getDiscoveryPlanErrorMessage(error, 'Failed to apply work cycle')
    });
  }
});

router.post('/:projectName/work-cycles/:cycleId/archive', async (req, res) => {
  try {
    const projectName = getTrimmedParam(req.params?.projectName);
    const cycleId = getTrimmedParam(req.params?.cycleId);
    if (!projectName) return res.status(400).json({ error: 'projectName is required' });
    if (!cycleId) return res.status(400).json({ error: 'cycleId is required' });

    const result = await archiveWorkCycle(projectName, cycleId);
    return res.json(result);
  } catch (error) {
    return res.status(getDiscoveryPlanErrorStatus(error)).json({
      error: getDiscoveryPlanErrorMessage(error, 'Failed to archive work cycle')
    });
  }
});

/**
 * Create a new workspace
 * POST /api/projects/create-workspace
 *
 * Body:
 * - workspaceType: 'existing' | 'new'
 * - path: string (workspace path)
 * - githubUrl?: string (optional, for new workspaces)
 * - githubTokenId?: number (optional, ID of stored token)
 * - newGithubToken?: string (optional, one-time token)
 */
router.post('/create-workspace', async (req, res) => {
  try {
    const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.body;

    // Validate required fields
    if (!workspaceType || !workspacePath) {
      return res.status(400).json({ error: 'workspaceType and path are required' });
    }

    if (!['existing', 'new'].includes(workspaceType)) {
      return res.status(400).json({ error: 'workspaceType must be "existing" or "new"' });
    }

    // Validate path safety before any operations
    const validation = await validateWorkspacePath(workspacePath);
    if (!validation.valid) {
      return res.status(400).json({
        error: 'Invalid workspace path',
        details: validation.error
      });
    }

    const absolutePath = validation.resolvedPath;

    // Handle existing workspace
    if (workspaceType === 'existing') {
      // Check if the path exists
      try {
        await fs.access(absolutePath);
        const stats = await fs.stat(absolutePath);

        if (!stats.isDirectory()) {
          return res.status(400).json({ error: 'Path exists but is not a directory' });
        }
      } catch (error) {
        if (error.code === 'ENOENT') {
          return res.status(404).json({ error: 'Workspace path does not exist' });
        }
        throw error;
      }

      // Add the existing workspace to the project list
      const project = await addProjectManually(absolutePath);

      return res.json({
        success: true,
        project,
        message: 'Existing workspace added successfully'
      });
    }

    // Handle new workspace creation
    if (workspaceType === 'new') {
      // Create the directory if it doesn't exist
      await fs.mkdir(absolutePath, { recursive: true });

      // If GitHub URL is provided, clone the repository
      if (githubUrl) {
        let githubToken = null;

        // Get GitHub token if needed
        if (githubTokenId) {
          // Fetch token from database
          const token = await getGithubTokenById(githubTokenId, req.user.id);
          if (!token) {
            // Clean up created directory
            await fs.rm(absolutePath, { recursive: true, force: true });
            return res.status(404).json({ error: 'GitHub token not found' });
          }
          githubToken = token.github_token;
        } else if (newGithubToken) {
          githubToken = newGithubToken;
        }

        // Extract repo name from URL for the clone destination
        const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
        const repoName = normalizedUrl.split('/').pop() || 'repository';
        const clonePath = path.join(absolutePath, repoName);

        // Check if clone destination already exists to prevent data loss
        try {
          await fs.access(clonePath);
          return res.status(409).json({
            error: 'Directory already exists',
            details: `The destination path "${clonePath}" already exists. Please choose a different location or remove the existing directory.`
          });
        } catch (err) {
          // Directory doesn't exist, which is what we want
        }

        // Clone the repository into a subfolder
        try {
          await cloneGitHubRepository(githubUrl, clonePath, githubToken);
        } catch (error) {
          // Only clean up if clone created partial data (check if dir exists and is empty or partial)
          try {
            const stats = await fs.stat(clonePath);
            if (stats.isDirectory()) {
              await fs.rm(clonePath, { recursive: true, force: true });
            }
          } catch (cleanupError) {
            // Directory doesn't exist or cleanup failed - ignore
          }
          throw new Error(`Failed to clone repository: ${error.message}`);
        }

        // Add the cloned repo path to the project list
        const project = await addProjectManually(clonePath);

        return res.json({
          success: true,
          project,
          message: 'New workspace created and repository cloned successfully'
        });
      }

      // Add the new workspace to the project list (no clone)
      const project = await addProjectManually(absolutePath);

      return res.json({
        success: true,
        project,
        message: 'New workspace created successfully'
      });
    }

  } catch (error) {
    console.error('Error creating workspace:', error);
    res.status(500).json({
      error: error.message || 'Failed to create workspace',
      details: process.env.NODE_ENV === 'development' ? error.stack : undefined
    });
  }
});

/**
 * Helper function to get GitHub token from database
 */
async function getGithubTokenById(tokenId, userId) {
  const { db } = await import('../database/db.js');

  const credential = db.prepare(
    'SELECT * FROM user_credentials WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1'
  ).get(tokenId, userId, 'github_token');

  // Return in the expected format (github_token field for compatibility)
  if (credential) {
    return {
      ...credential,
      github_token: credential.credential_value
    };
  }

  return null;
}

/**
 * Clone repository with progress streaming (SSE)
 * GET /api/projects/clone-progress
 */
router.get('/clone-progress', async (req, res) => {
  const { path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.query;

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.flushHeaders();

  const sendEvent = (type, data) => {
    res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
  };

  try {
    if (!workspacePath || !githubUrl) {
      sendEvent('error', { message: 'workspacePath and githubUrl are required' });
      res.end();
      return;
    }

    const validation = await validateWorkspacePath(workspacePath);
    if (!validation.valid) {
      sendEvent('error', { message: validation.error });
      res.end();
      return;
    }

    const absolutePath = validation.resolvedPath;

    await fs.mkdir(absolutePath, { recursive: true });

    let githubToken = null;
    if (githubTokenId) {
      const token = await getGithubTokenById(parseInt(githubTokenId), req.user.id);
      if (!token) {
        await fs.rm(absolutePath, { recursive: true, force: true });
        sendEvent('error', { message: 'GitHub token not found' });
        res.end();
        return;
      }
      githubToken = token.github_token;
    } else if (newGithubToken) {
      githubToken = newGithubToken;
    }

    const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
    const repoName = normalizedUrl.split('/').pop() || 'repository';
    const clonePath = path.join(absolutePath, repoName);

    // Check if clone destination already exists to prevent data loss
    try {
      await fs.access(clonePath);
      sendEvent('error', { message: `Directory "${repoName}" already exists. Please choose a different location or remove the existing directory.` });
      res.end();
      return;
    } catch (err) {
      // Directory doesn't exist, which is what we want
    }

    let cloneUrl = githubUrl;
    if (githubToken) {
      try {
        const url = new URL(githubUrl);
        url.username = githubToken;
        url.password = '';
        cloneUrl = url.toString();
      } catch (error) {
        // SSH URL or invalid - use as-is
      }
    }

    sendEvent('progress', { message: `Cloning into '${repoName}'...` });

    const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, clonePath], {
      stdio: ['ignore', 'pipe', 'pipe'],
      env: {
        ...process.env,
        GIT_TERMINAL_PROMPT: '0'
      }
    });

    let lastError = '';

    gitProcess.stdout.on('data', (data) => {
      const message = data.toString().trim();
      if (message) {
        sendEvent('progress', { message });
      }
    });

    gitProcess.stderr.on('data', (data) => {
      const message = data.toString().trim();
      lastError = message;
      if (message) {
        sendEvent('progress', { message });
      }
    });

    gitProcess.on('close', async (code) => {
      if (code === 0) {
        try {
          const project = await addProjectManually(clonePath);
          sendEvent('complete', { project, message: 'Repository cloned successfully' });
        } catch (error) {
          sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` });
        }
      } else {
        const sanitizedError = sanitizeGitError(lastError, githubToken);
        let errorMessage = 'Git clone failed';
        if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) {
          errorMessage = 'Authentication failed. Please check your credentials.';
        } else if (lastError.includes('Repository not found')) {
          errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
        } else if (lastError.includes('already exists')) {
          errorMessage = 'Directory already exists';
        } else if (sanitizedError) {
          errorMessage = sanitizedError;
        }
        try {
          await fs.rm(clonePath, { recursive: true, force: true });
        } catch (cleanupError) {
          console.error('Failed to clean up after clone failure:', sanitizeGitError(cleanupError.message, githubToken));
        }
        sendEvent('error', { message: errorMessage });
      }
      res.end();
    });

    gitProcess.on('error', (error) => {
      if (error.code === 'ENOENT') {
        sendEvent('error', { message: 'Git is not installed or not in PATH' });
      } else {
        sendEvent('error', { message: error.message });
      }
      res.end();
    });

    req.on('close', () => {
      gitProcess.kill();
    });

  } catch (error) {
    sendEvent('error', { message: error.message });
    res.end();
  }
});

/**
 * Helper function to clone a GitHub repository
 */
function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
  return new Promise((resolve, reject) => {
    let cloneUrl = githubUrl;

    if (githubToken) {
      try {
        const url = new URL(githubUrl);
        url.username = githubToken;
        url.password = '';
        cloneUrl = url.toString();
      } catch (error) {
        // SSH URL - use as-is
      }
    }

    const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], {
      stdio: ['ignore', 'pipe', 'pipe'],
      env: {
        ...process.env,
        GIT_TERMINAL_PROMPT: '0'
      }
    });

    let stdout = '';
    let stderr = '';

    gitProcess.stdout.on('data', (data) => {
      stdout += data.toString();
    });

    gitProcess.stderr.on('data', (data) => {
      stderr += data.toString();
    });

    gitProcess.on('close', (code) => {
      if (code === 0) {
        resolve({ stdout, stderr });
      } else {
        let errorMessage = 'Git clone failed';

        if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) {
          errorMessage = 'Authentication failed. Please check your GitHub token.';
        } else if (stderr.includes('Repository not found')) {
          errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
        } else if (stderr.includes('already exists')) {
          errorMessage = 'Directory already exists';
        } else if (stderr) {
          errorMessage = stderr;
        }

        reject(new Error(errorMessage));
      }
    });

    gitProcess.on('error', (error) => {
      if (error.code === 'ENOENT') {
        reject(new Error('Git is not installed or not in PATH'));
      } else {
        reject(error);
      }
    });
  });
}

export default router;
