#pragma once

#include "../include/password.hpp"

#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <openssl/sha.h>
#include <print>
#include <ranges>
#include <sstream>

namespace sec {

sec::password::password(std::string pass) {
  if (pass.size() > _buf.size()) {
    throw std::length_error("Password too long");
  }
  _len = pass.size();
  std::copy(pass.data(), pass.data() + _len, _buf.begin());
}

std::string_view sec::password::view() const noexcept {
  return std::string_view(_buf.data(), _len);
}

sec::password sec::password::from_console() {
  std::string p1, p2;
  while (true) {
    std::print("Enter password: ");
    std::getline(std::cin, p1);
    std::print("Re-enter password: ");
    std::getline(std::cin, p2);
    if (p1 != p2) {
      std::print("Passwords do not match. Try again.\n");
      continue;
    }
    if (sec::manager::evaluate_strength(sec::password(p1)) ==
        sec::strength_t::very_weak) {
      std::print("Password is too weak. Try again.\n");
      continue;
    }
    break;
  }
  return sec::password(p1);
}

sec::password sec::password::from_string(const std::string &pass) {
  if (sec::manager::evaluate_strength(sec::password(pass)) ==
      sec::strength_t::very_weak) {
    std::print("Password is too weak. Try again.\n\n");
    throw std::invalid_argument("Weak password");
  }
  return sec::password(pass);
}

sec::password_hash::password_hash(std::string hash) : _hash(std::move(hash)) {}

const std::string &sec::password_hash::get() const noexcept { return _hash; }

bool sec::password_hash::verify(const sec::password &pwd) const {
  return sec::manager::hash_password(pwd) == _hash;
}

sec::password_hash sec::password::to_hash() const {
  return sec::password_hash(sec::manager::hash_password(*this));
}

bool sec::manager::has_lower(const sec::password &pwd) {
  auto sv = pwd.view();
  return std::any_of(sv.begin(), sv.end(), ::islower);
}

bool sec::manager::has_upper(const sec::password &pwd) {
  auto sv = pwd.view();
  return std::any_of(sv.begin(), sv.end(), ::isupper);
}

bool sec::manager::has_digit(const sec::password &pwd) {
  auto sv = pwd.view();
  return std::any_of(sv.begin(), sv.end(), ::isdigit);
}

bool sec::manager::has_spec_symbol(const sec::password &pwd) {
  auto sv = pwd.view();
  return std::any_of(sv.begin(), sv.end(),
                     [](unsigned char c) { return std::ispunct(c); });
}

sec::strength_t sec::manager::evaluate_strength(const sec::password &pwd) {
  uint8_t score = 0;

  if (has_lower(pwd))
    ++score;
  if (has_upper(pwd))
    ++score;
  if (has_digit(pwd))
    ++score;
  if (has_spec_symbol(pwd))
    ++score;
  if (pwd.view().size() >= 8)
    ++score;

  if (score != 0)
    score--;

  uint8_t max_val = static_cast<uint8_t>(sec::strength_t::very_strong);
  uint8_t idx = std::min(score, max_val);

  return static_cast<sec::strength_t>(idx);
}

uint64_t sec::manager::time_to_crack(const sec::password &pwd) {
  auto sv = pwd.view();
  if (sv.empty())
    return 0;
  int charset = (has_lower(pwd) ? 26 : 0) + (has_upper(pwd) ? 26 : 0) +
                (has_digit(pwd) ? 10 : 0) + (has_spec_symbol(pwd) ? 33 : 0);
  constexpr double RATE = 1e7;
  double combos = std::pow(static_cast<double>(charset), sv.size());
  return static_cast<uint64_t>(combos / 2.0 / RATE);
}

std::string sec::manager::format_duration(uint64_t seconds) {
  const uint64_t u[] = {31536000, 2592000, 86400, 3600, 60, 1};
  const char *lbl[] = {"years", "months", "days", "hours", "mins", "secs"};
  std::string s;
  for (int i : std::views::iota(0, 6)) {
    uint64_t v = seconds / u[i];
    if (v) {
      s += std::to_string(v) + " " + lbl[i] + ((i == 5) ? "" : " ");
      seconds %= u[i];
    }
  }
  return s.empty() ? "0 secs" : s;
}

std::string sec::manager::hash_password(const sec::password &pwd) {
  auto sv = pwd.view();
  unsigned char hash[SHA256_DIGEST_LENGTH];
  SHA256(reinterpret_cast<const unsigned char *>(sv.data()), sv.size(), hash);
  std::ostringstream oss;
  for (int32_t i : std::views::iota(0, SHA256_DIGEST_LENGTH)) {
    oss << std::hex << std::setw(2) << std::setfill('0')
        << static_cast<int32_t>(hash[i]);
  }
  return oss.str();
}

} // namespace sec
