/**
 * Hosting Connections API — /api/hosting/connections
 *
 * Per-admin connections to hosting providers (Netlify, Vercel, GitHub Pages, SFTP,
 * FTP, S3) used to publish landing pages. Secrets are encrypted at rest and never
 * returned. Ownership is scoped to (companyId, userId): admins manage their own
 * connections; only the creator can mutate/delete.
 *
 * Fully additive — new collection, new route namespace. Nothing existing is touched.
 */

import express, { Request, Response } from 'express';
import { authenticate } from '../middleware/auth';
import { requirePermission } from '../middleware/permissions';
import { getModels } from '../models';
import { encrypt } from '../utils/encryption';
import { readConnectionSecret } from '../services/hosting/connectionSecret';
import { getHostingAdapter, SUPPORTED_HOSTING_PROVIDERS } from '../services/hosting/adapters';
import { normalizeHost } from '../services/hosting/adapters/types';

const router = express.Router();
router.use(authenticate);

/** Providers that connect over a raw host/port rather than an HTTPS API. */
const HOST_BASED_PROVIDERS = ['sftp', 'ftp'];

/** Providers whose "host" field stores a full URL rather than a bare hostname. */
const URL_BASED_PROVIDERS = ['wordpress'];

/**
 * Clean up host/port/remotePath before they're stored.
 *
 * For SFTP/FTP: The Host field is routinely filled in with a full URL, which DNS can't
 * resolve (`getaddrinfo ENOTFOUND https://host/`). Normalising on write means the saved
 * connection is correct for every later validate and deploy, not just this one.
 *
 * For WordPress: The Host field stores the full site URL (e.g. https://myblog.com).
 * Only trailing slashes are stripped — the scheme must be preserved.
 *
 * remotePath is only trimmed, never defaulted: the adapters need to tell "user left it
 * blank" (prefer public_html, but fall back to the login directory when it doesn't
 * exist) apart from "user asked for public_html" (always use it). Writing the default
 * here would collapse those two cases and force a /public_html/ subfolder on hosts whose
 * FTP login already lands in the web root.
 */
function normalizeConnectionTarget(provider: string, body: any, target: Record<string, any>): void {
  if (URL_BASED_PROVIDERS.includes(provider)) {
    // WordPress: store the site URL as-is, just strip trailing slashes
    if (body.host !== undefined) {
      target.host = String(body.host).replace(/\/+$/, '').trim();
    }
    return;
  }
  if (!HOST_BASED_PROVIDERS.includes(provider)) return;
  if (body.host !== undefined) {
    const { host, port } = normalizeHost(body.host);
    target.host = host;
    // Honour a port embedded in the host ("example.com:2222") when none was given.
    if (!body.port && port) target.port = port;
  }
  if (body.remotePath !== undefined) {
    target.remotePath = String(body.remotePath).trim();
  }
}

const authorizeCompany = (req: Request, companyId: string): boolean =>
  req.user!.companyIds.includes(companyId) || req.user!.role === 'admin' || req.user!.role === 'super-admin';

/** Build the secret string from either a raw `secret` or a `credentials` object. */
function secretFromBody(body: any): string {
  if (body.credentials && typeof body.credentials === 'object') return JSON.stringify(body.credentials);
  if (typeof body.secret === 'string') return body.secret;
  return '';
}

// List a company's connections (owned by the requesting admin).
router.get('/:companyId', async (req: Request, res: Response) => {
  try {
    const { companyId } = req.params;
    if (!authorizeCompany(req, companyId)) { res.status(403).json({ error: 'Access denied' }); return; }
    const { HostingConnection } = getModels();
    const userId = req.user!._id.toString();
    const list = await HostingConnection.find({ companyId, userId }).sort({ createdAt: -1 });
    res.json(list);
  } catch (error: any) {
    res.status(500).json({ error: error?.message || 'Failed to list connections' });
  }
});

// Create a connection.
router.post('/', requirePermission('landing-pages', 'edit'), async (req: Request, res: Response) => {
  try {
    const { companyId, provider, label } = req.body;
    if (!companyId || !authorizeCompany(req, companyId)) { res.status(403).json({ error: 'Access denied' }); return; }
    if (!SUPPORTED_HOSTING_PROVIDERS.includes(provider)) { res.status(400).json({ error: 'Unsupported provider' }); return; }
    if (!label) { res.status(400).json({ error: 'A label is required' }); return; }

    const { HostingConnection } = getModels();
    const secret = secretFromBody(req.body);

    const target: Record<string, any> = {
      host: req.body.host, port: req.body.port,
      remotePath: req.body.remotePath,
    };
    normalizeConnectionTarget(provider, req.body, target);

    const conn = new HostingConnection({
      companyId,
      userId: req.user!._id.toString(),
      provider,
      label,
      encryptedSecret: secret ? encrypt(secret) : undefined,
      host: target.host, port: target.port, username: req.body.username,
      remotePath: target.remotePath, baseUrl: req.body.baseUrl,
      siteId: req.body.siteId, teamId: req.body.teamId, repo: req.body.repo,
      branch: req.body.branch, region: req.body.region, bucket: req.body.bucket,
      status: 'connected',
    });
    await conn.save();
    const obj = conn.toObject();
    delete (obj as any).encryptedSecret;
    res.status(201).json(obj);
  } catch (error: any) {
    res.status(500).json({ error: error?.message || 'Failed to create connection' });
  }
});

// Update non-secret fields (and optionally rotate the secret).
router.patch('/:id', requirePermission('landing-pages', 'edit'), async (req: Request, res: Response) => {
  try {
    const { HostingConnection } = getModels();
    const conn = await HostingConnection.findById(req.params.id);
    if (!conn || conn.userId !== req.user!._id.toString()) { res.status(404).json({ error: 'Connection not found' }); return; }

    const updatable = ['label', 'host', 'port', 'username', 'remotePath', 'baseUrl', 'siteId', 'teamId', 'repo', 'branch', 'region', 'bucket'];
    for (const k of updatable) if (req.body[k] !== undefined) (conn as any)[k] = req.body[k];
    normalizeConnectionTarget(conn.provider, req.body, conn as any);
    const newSecret = secretFromBody(req.body);
    if (newSecret) {
      conn.encryptedSecret = encrypt(newSecret);
      // The new value is CBC, which carries its own IV. A leftover
      // providerData.iv from a WordPress connection saved in Settings →
      // Integrations would make this row read as the other cipher.
      if (conn.providerData?.iv) {
        const { iv, ...rest } = conn.providerData as Record<string, any>;
        conn.providerData = rest;
        conn.markModified('providerData');
      }
    }
    conn.status = 'connected';
    conn.lastError = undefined;
    await conn.save();
    const obj = conn.toObject();
    delete (obj as any).encryptedSecret;
    res.json(obj);
  } catch (error: any) {
    res.status(500).json({ error: error?.message || 'Failed to update connection' });
  }
});

// Validate a connection by probing the provider.
router.post('/:id/validate', requirePermission('landing-pages', 'edit'), async (req: Request, res: Response) => {
  try {
    const { HostingConnection } = getModels();
    const conn = await HostingConnection.findById(req.params.id).select('+encryptedSecret');
    if (!conn || conn.userId !== req.user!._id.toString()) { res.status(404).json({ error: 'Connection not found' }); return; }
    const adapter = getHostingAdapter(conn.provider);
    if (!adapter || !adapter.validate) { res.json({ ok: true, note: 'No validation available for this provider' }); return; }
    // Reads either cipher — a WordPress connection may have been written by the
    // Settings → Integrations screen, which stores it differently.
    const secret = readConnectionSecret(conn);
    const result = await adapter.validate(conn, secret);
    conn.status = result.ok ? 'connected' : 'reconnect_required';
    conn.lastError = result.ok ? undefined : result.error;
    await conn.save().catch(() => undefined);
    res.json(result);
  } catch (error: any) {
    res.status(500).json({ error: error?.message || 'Validation failed' });
  }
});

// Delete a connection (owner only).
router.delete('/:id', requirePermission('landing-pages', 'edit'), async (req: Request, res: Response) => {
  try {
    const { HostingConnection } = getModels();
    const conn = await HostingConnection.findById(req.params.id);
    if (!conn || conn.userId !== req.user!._id.toString()) { res.status(404).json({ error: 'Connection not found' }); return; }
    await HostingConnection.deleteOne({ _id: conn._id });
    res.json({ success: true });
  } catch (error: any) {
    res.status(500).json({ error: error?.message || 'Failed to delete connection' });
  }
});

export default router;
