const path = require('path');
const fs = require('fs/promises');
const crypto = require('crypto');

const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const multer = require('multer');
const sharp = require('sharp');
require('dotenv').config();

const { query } = require('./db');

const app = express();
const PORT = process.env.PORT || 3000;
const uploadDir = path.join(__dirname, 'uploads');

const allowedMimeTypes = new Set([
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
  'image/heic',
  'image/heif',
  'image/heic-sequence',
  'image/heif-sequence',
]);

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    if (!allowedMimeTypes.has(file.mimetype)) {
      return cb(new Error('Unsupported file type.'));
    }
    return cb(null, true);
  },
});

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/uploads', express.static(uploadDir));
app.use('/android', express.static(path.join(__dirname, 'android')));
app.use('/ios', express.static(path.join(__dirname, 'ios')));

app.use(
  session({
    secret: process.env.SESSION_SECRET || 'dev-secret',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 1000 * 60 * 60 * 24 * 365 },
  })
);

app.use((req, res, next) => {
  res.locals.currentUser = req.session.user || null;
  res.locals.flash = req.session.flash || null;
  delete req.session.flash;
  next();
});

function requireAuth(req, res, next) {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  return next();
}

function requireApproved(req, res, next) {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  if (!req.session.user.is_approved) {
    return res.redirect('/pending');
  }
  return next();
}

function requireAdmin(req, res, next) {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  if (!req.session.user.is_admin) {
    return res.status(403).render('error', {
      title: 'Not authorized',
      message: 'You do not have access to this page.',
    });
  }
  return next();
}

app.get('/', async (req, res, next) => {
  try {
    let pads = [];
    if (req.session.user) {
      const { rows } = await query(
        `SELECT p.id, p.name, p.brand, p.description,
                r.fit, r.comfort, r.absorbency
         FROM pads p
         LEFT JOIN reviews r
           ON r.pad_id = p.id AND r.user_id = p.user_id
         WHERE p.user_id = $1
         ORDER BY p.name`,
        [req.session.user.id]
      );
      pads = rows;
    }
    res.render('index', { title: 'Pedal', pads });
  } catch (err) {
    next(err);
  }
});

app.get('/pads/new', requireApproved, (req, res) => {
  res.render('new-pad', { title: 'Add a pad' });
});

app.post('/pads', requireApproved, async (req, res, next) => {
  const name = req.body.name?.trim();
  const brand = req.body.brand?.trim() || null;
  const description = req.body.description?.trim() || null;

  if (!name) {
    req.session.flash = { type: 'danger', message: 'Pad name is required.' };
    return res.redirect('/pads/new');
  }

  try {
    const { rows } = await query(
      `INSERT INTO pads (name, brand, description, user_id)
       VALUES ($1, $2, $3, $4)
       RETURNING id`,
      [name, brand, description, req.session.user.id]
    );
    req.session.flash = { type: 'success', message: 'Pad added.' };
    return res.redirect(`/pads/${rows[0].id}`);
  } catch (err) {
    if (err.code === '23505') {
      req.session.flash = { type: 'danger', message: 'That pad already exists.' };
      return res.redirect('/pads/new');
    }
    return next(err);
  }
});

app.get('/register', (req, res) => {
  res.render('register', { title: 'Create account' });
});

app.post('/register', async (req, res, next) => {
  const { username, password } = req.body;
  if (!username || !password) {
    req.session.flash = { type: 'danger', message: 'Username and password required.' };
    return res.redirect('/register');
  }

  try {
    const existing = await query('SELECT id FROM users WHERE username = $1', [
      username,
    ]);
    if (existing.rows.length > 0) {
      req.session.flash = { type: 'danger', message: 'Username already taken.' };
      return res.redirect('/register');
    }

    const passwordHash = await bcrypt.hash(password, 12);
    const { rows } = await query(
      `INSERT INTO users (username, password_hash)
       VALUES ($1, $2)
       RETURNING id, username, is_admin, is_approved`,
      [username, passwordHash]
    );

    req.session.user = rows[0];
    req.session.flash = {
      type: 'info',
      message: 'Account created. Awaiting admin approval.',
    };
    return res.redirect('/pending');
  } catch (err) {
    return next(err);
  }
});

app.get('/login', (req, res) => {
  res.render('login', { title: 'Sign in' });
});

app.post('/login', async (req, res, next) => {
  const { username, password } = req.body;
  try {
    const { rows } = await query(
      'SELECT id, username, password_hash, is_admin, is_approved FROM users WHERE username = $1',
      [username]
    );
    if (rows.length === 0) {
      req.session.flash = { type: 'danger', message: 'Invalid credentials.' };
      return res.redirect('/login');
    }

    const user = rows[0];
    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) {
      req.session.flash = { type: 'danger', message: 'Invalid credentials.' };
      return res.redirect('/login');
    }

    req.session.user = {
      id: user.id,
      username: user.username,
      is_admin: user.is_admin,
      is_approved: user.is_approved,
    };

    if (!user.is_approved) {
      req.session.flash = {
        type: 'warning',
        message: 'Your account is pending admin approval.',
      };
      return res.redirect('/pending');
    }

    return res.redirect('/');
  } catch (err) {
    return next(err);
  }
});

app.post('/logout', (req, res) => {
  req.session.destroy(() => {
    res.redirect('/');
  });
});

app.get('/pending', requireAuth, (req, res) => {
  res.render('pending', { title: 'Approval pending' });
});

app.get('/pads/:id', requireAuth, async (req, res, next) => {
  try {
    const padId = Number.parseInt(req.params.id, 10);
    if (Number.isNaN(padId)) {
      return res.status(404).render('error', {
        title: 'Not found',
        message: 'Pad not found.',
      });
    }

    const { rows: padRows } = await query(
      'SELECT id, name, brand, description FROM pads WHERE id = $1 AND user_id = $2',
      [padId, req.session.user.id]
    );
    if (padRows.length === 0) {
      return res.status(404).render('error', {
        title: 'Not found',
        message: 'Pad not found.',
      });
    }

    const { rows: reviewRows } = await query(
      `SELECT id, fit, comfort, absorbency, notes, created_at, updated_at
       FROM reviews
       WHERE pad_id = $1 AND user_id = $2`,
      [padId, req.session.user.id]
    );

    const { rows: photos } = await query(
      `SELECT id, filename, original_name, created_at
       FROM photos
       WHERE pad_id = $1 AND user_id = $2
       ORDER BY created_at DESC`,
      [padId, req.session.user.id]
    );

    return res.render('pad', {
      title: padRows[0].name,
      pad: padRows[0],
      review: reviewRows[0] || null,
      photos,
    });
  } catch (err) {
    return next(err);
  }
});

app.post('/pads/:id/delete', requireApproved, async (req, res, next) => {
  try {
    const padId = Number.parseInt(req.params.id, 10);
    if (Number.isNaN(padId)) {
      return res.status(404).render('error', {
        title: 'Not found',
        message: 'Pad not found.',
      });
    }

    const { rows: padRows } = await query(
      'SELECT id FROM pads WHERE id = $1 AND user_id = $2',
      [padId, req.session.user.id]
    );
    if (padRows.length === 0) {
      return res.status(404).render('error', {
        title: 'Not found',
        message: 'Pad not found.',
      });
    }

    const { rows: photoRows } = await query(
      'SELECT filename FROM photos WHERE pad_id = $1 AND user_id = $2',
      [padId, req.session.user.id]
    );

    await query('DELETE FROM pads WHERE id = $1 AND user_id = $2', [
      padId,
      req.session.user.id,
    ]);

    for (const photo of photoRows) {
      try {
        await fs.unlink(path.join(uploadDir, photo.filename));
      } catch (err) {
        if (err.code !== 'ENOENT') {
          throw err;
        }
      }
    }

    req.session.flash = { type: 'success', message: 'Pad deleted.' };
    return res.redirect('/');
  } catch (err) {
    return next(err);
  }
});

app.post('/pads/:id/reviews', requireApproved, async (req, res, next) => {
  const padId = Number.parseInt(req.params.id, 10);
  const fit = Number.parseInt(req.body.fit, 10);
  const comfort = Number.parseInt(req.body.comfort, 10);
  const absorbency = Number.parseInt(req.body.absorbency, 10);
  const notes = req.body.notes?.trim() || null;

  if ([fit, comfort, absorbency].some((value) => Number.isNaN(value))) {
    req.session.flash = { type: 'danger', message: 'Please score all rating fields.' };
    return res.redirect(`/pads/${padId}`);
  }

  if ([fit, comfort, absorbency].some((value) => value < 1 || value > 5)) {
    req.session.flash = { type: 'danger', message: 'Ratings must be between 1 and 5.' };
    return res.redirect(`/pads/${padId}`);
  }

  try {
    const { rows: padRows } = await query(
      'SELECT id FROM pads WHERE id = $1 AND user_id = $2',
      [padId, req.session.user.id]
    );
    if (padRows.length === 0) {
      return res.status(404).render('error', {
        title: 'Not found',
        message: 'Pad not found.',
      });
    }

    await query(
      `INSERT INTO reviews (user_id, pad_id, fit, comfort, absorbency, notes)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (user_id, pad_id)
       DO UPDATE SET
         fit = EXCLUDED.fit,
         comfort = EXCLUDED.comfort,
         absorbency = EXCLUDED.absorbency,
         notes = EXCLUDED.notes,
         updated_at = NOW()`,
      [req.session.user.id, padId, fit, comfort, absorbency, notes]
    );
    req.session.flash = { type: 'success', message: 'Review saved.' };
    return res.redirect(`/pads/${padId}`);
  } catch (err) {
    return next(err);
  }
});

app.post(
  '/pads/:id/photos',
  requireApproved,
  (req, res, next) => {
    upload.single('photo')(req, res, (err) => {
      if (err) {
        req.session.flash = { type: 'danger', message: err.message };
        return res.redirect(`/pads/${req.params.id}`);
      }
      return next();
    });
  },
  async (req, res, next) => {
    const padId = Number.parseInt(req.params.id, 10);
    if (!req.file) {
      req.session.flash = { type: 'danger', message: 'Please select a photo to upload.' };
      return res.redirect(`/pads/${padId}`);
    }

    try {
      const { rows: padRows } = await query(
        'SELECT id FROM pads WHERE id = $1 AND user_id = $2',
        [padId, req.session.user.id]
      );
      if (padRows.length === 0) {
        return res.status(404).render('error', {
          title: 'Not found',
          message: 'Pad not found.',
        });
      }

      const filename = `${crypto.randomUUID()}.webp`;
      const outputPath = path.join(uploadDir, filename);

      await sharp(req.file.buffer)
        .rotate()
        .resize(1600, 1600, { fit: 'inside', withoutEnlargement: true })
        .webp({ quality: 80 })
        .toFile(outputPath);

      await query(
        `INSERT INTO photos (user_id, pad_id, filename, original_name)
         VALUES ($1, $2, $3, $4)`,
        [req.session.user.id, padId, filename, req.file.originalname]
      );

      req.session.flash = { type: 'success', message: 'Photo uploaded.' };
      return res.redirect(`/pads/${padId}`);
    } catch (err) {
      return next(err);
    }
  }
);

app.post(
  '/pads/:padId/photos/:photoId/delete',
  requireApproved,
  async (req, res, next) => {
    try {
      const padId = Number.parseInt(req.params.padId, 10);
      const photoId = Number.parseInt(req.params.photoId, 10);
      if (Number.isNaN(padId) || Number.isNaN(photoId)) {
        return res.status(404).render('error', {
          title: 'Not found',
          message: 'Photo not found.',
        });
      }

      const { rows } = await query(
        'SELECT filename FROM photos WHERE id = $1 AND pad_id = $2 AND user_id = $3',
        [photoId, padId, req.session.user.id]
      );
      if (rows.length === 0) {
        return res.status(404).render('error', {
          title: 'Not found',
          message: 'Photo not found.',
        });
      }

      await query('DELETE FROM photos WHERE id = $1 AND pad_id = $2 AND user_id = $3', [
        photoId,
        padId,
        req.session.user.id,
      ]);

      try {
        await fs.unlink(path.join(uploadDir, rows[0].filename));
      } catch (err) {
        if (err.code !== 'ENOENT') {
          throw err;
        }
      }

      req.session.flash = { type: 'success', message: 'Photo deleted.' };
      return res.redirect(`/pads/${padId}`);
    } catch (err) {
      return next(err);
    }
  }
);

app.get('/admin', requireAdmin, async (req, res, next) => {
  try {
    const { rows: pendingUsers } = await query(
      `SELECT id, username, created_at
       FROM users
       WHERE is_approved = FALSE
       ORDER BY created_at ASC`
    );
    const { rows: users } = await query(
      `SELECT id, username, is_admin, is_approved, created_at
       FROM users
       ORDER BY created_at DESC`
    );
    res.render('admin', { title: 'Admin', pendingUsers, users });
  } catch (err) {
    next(err);
  }
});

app.post('/admin/users/:id/approve', requireAdmin, async (req, res, next) => {
  try {
    await query('UPDATE users SET is_approved = TRUE WHERE id = $1', [
      req.params.id,
    ]);
    req.session.flash = { type: 'success', message: 'User approved.' };
    res.redirect('/admin');
  } catch (err) {
    next(err);
  }
});

app.post('/admin/users/:id/reset-password', requireAdmin, async (req, res, next) => {
  const newPassword = req.body.password?.trim();
  if (!newPassword) {
    req.session.flash = { type: 'danger', message: 'Password cannot be empty.' };
    return res.redirect('/admin');
  }

  try {
    const passwordHash = await bcrypt.hash(newPassword, 12);
    await query('UPDATE users SET password_hash = $1 WHERE id = $2', [
      passwordHash,
      req.params.id,
    ]);
    req.session.flash = { type: 'success', message: 'Password reset.' };
    return res.redirect('/admin');
  } catch (err) {
    return next(err);
  }
});

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).render('error', {
    title: 'Something went wrong',
    message: 'Please try again or check the logs.',
  });
});

async function ensureAdmin() {
  const adminUsername = process.env.ADMIN_USERNAME;
  const adminPassword = process.env.ADMIN_PASSWORD;
  if (!adminUsername || !adminPassword) {
    return;
  }

  const { rows } = await query('SELECT id FROM users WHERE username = $1', [
    adminUsername,
  ]);
  if (rows.length > 0) {
    return;
  }

  const passwordHash = await bcrypt.hash(adminPassword, 12);
  await query(
    `INSERT INTO users (username, password_hash, is_admin, is_approved)
     VALUES ($1, $2, TRUE, TRUE)`,
    [adminUsername, passwordHash]
  );
}

async function waitForDatabase(retries = 12) {
  const delayMs = 1000;
  for (let attempt = 1; attempt <= retries; attempt += 1) {
    try {
      await query('SELECT 1');
      return;
    } catch (err) {
      if (attempt === retries) {
        throw err;
      }
      await new Promise((resolve) => setTimeout(resolve, delayMs));
    }
  }
}

async function start() {
  await fs.mkdir(uploadDir, { recursive: true });
  await waitForDatabase();
  await ensureAdmin();

  app.listen(PORT, () => {
    console.log(`Pedal running on http://localhost:${PORT}`);
  });
}

start().catch((err) => {
  console.error(err);
  process.exit(1);
});
