/*
 * kmscon - /etc/issue display
 *
 * Copyright (c) 2025 Alberto Ruiz <aruiz@redhat.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <glob.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <time.h>
#include "issue.h"
#include "issue_network.h"
#include "shl/log.h"

/* Cap total collected issue text to prevent runaway allocation. */
#define ISSUE_MAX_SIZE (256u * 1024u)

/* Color escape codes */
// clang-format off
#define UL_COLOR_RESET		"[0m"
#define UL_COLOR_BOLD		"[1m"
#define UL_COLOR_HALFBRIGHT	"[2m"
#define UL_COLOR_UNDERSCORE	"[4m"
#define UL_COLOR_BLINK		"[5m"
#define UL_COLOR_REVERSE	"[7m"

/* Standard colors */
#define UL_COLOR_BLACK		"[30m"
#define UL_COLOR_RED		"[31m"
#define UL_COLOR_GREEN		"[32m"
#define UL_COLOR_BROWN		"[33m"
#define UL_COLOR_BLUE		"[34m"
#define UL_COLOR_MAGENTA	"[35m"
#define UL_COLOR_CYAN		"[36m"
#define UL_COLOR_GRAY		"[37m"

/* Bold variants */
#define UL_COLOR_DARK_GRAY	"[1;30m"
#define UL_COLOR_BOLD_RED	"[1;31m"
#define UL_COLOR_BOLD_GREEN	"[1;32m"
#define UL_COLOR_BOLD_YELLOW	"[1;33m"
#define UL_COLOR_BOLD_BLUE	"[1;34m"
#define UL_COLOR_BOLD_MAGENTA	"[1;35m"
#define UL_COLOR_BOLD_CYAN	"[1;36m"
#define UL_COLOR_WHITE		"[1;37m"
// clang-format on

#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))

static char *read_os_release_field(const char *field)
{
	FILE *fp;
	char line[512];
	size_t flen;

	fp = fopen("/etc/os-release", "r");
	if (!fp)
		fp = fopen("/usr/lib/os-release", "r");
	if (!fp)
		return NULL;

	flen = strlen(field);
	if (flen >= sizeof(line)) {
		fclose(fp);
		return NULL;
	}
	while (fgets(line, sizeof(line), fp)) {
		char *val;
		size_t vlen;

		if (strncmp(line, field, flen) != 0 || line[flen] != '=')
			continue;

		val = &line[flen + 1];
		vlen = strlen(val);
		if (vlen > 0 && val[vlen - 1] == '\n')
			val[--vlen] = '\0';
		if (vlen >= 2 && val[0] == '"' && val[vlen - 1] == '"') {
			val++;
			val[vlen - 2] = '\0';
		}
		fclose(fp);
		return strdup(val);
	}

	fclose(fp);
	return NULL;
}

#define MAX_FILES 64
#define CHUNK_SIZE 4096
struct issue_parser {
	size_t limit;
	char *parsed[MAX_FILES];
	int n_parsed_files;
};

static const char *bname(const char *path)
{
	const char *p = strrchr(path, '/');
	return p ? p + 1 : (char *)path;
}

static bool is_masked(const char *path, const struct issue_parser *parser)
{
	const char *b = bname(path);

	for (int i = 0; i < parser->n_parsed_files; i++)
		if (strcmp(b, parser->parsed[i]) == 0)
			return true;
	return false;
}

static void add_parsed_file(struct issue_parser *parser, const char *path)
{
	if (parser->n_parsed_files < MAX_FILES - 1)
		parser->parsed[parser->n_parsed_files++] = strdup(bname(path));
}

static void free_parser(struct issue_parser *parser)
{
	for (int i = 0; i < parser->n_parsed_files; i++)
		free(parser->parsed[i]);
	parser->n_parsed_files = 0;
}

static void append_file(FILE *out, const char *path, struct issue_parser *parser)
{
	FILE *fp;
	char buf[CHUNK_SIZE];
	size_t n;

	if (!parser->limit)
		return;

	// Ignore files that have the same name as a parsed file
	// so /etc/issue takes precedence over /run/issue and /usr/lib/issue
	if (is_masked(path, parser)) {
		log_debug("Ignoring masked file: %s", path);
		return;
	}

	fp = fopen(path, "r");
	if (!fp)
		return;

	add_parsed_file(parser, path);

	while (parser->limit) {
		size_t chunk = CHUNK_SIZE;
		if (chunk > parser->limit)
			chunk = parser->limit;
		n = fread(buf, 1, chunk, fp);
		if (!n)
			break;
		fwrite(buf, 1, n, out);
		parser->limit -= n;
	}
	log_debug("Parse issue file %s", path);
	fclose(fp);
}

static void append_dir(FILE *out, const char *dir, struct issue_parser *parser)
{
	glob_t gl;
	char *pattern;
	int i;

	if (asprintf(&pattern, "%s/*.issue", dir) < 0)
		return;

	if (glob(pattern, 0, NULL, &gl) != 0) {
		free(pattern);
		return;
	}
	free(pattern);

	for (i = 0; i < gl.gl_pathc && parser->limit; i++)
		append_file(out, gl.gl_pathv[i], parser);

	globfree(&gl);
}

/*
 * Collect raw issue text from all search path entries into a single buffer.
 * @search_path is a colon-separated list of files and directories.
 * Returns a malloc'd string (caller frees) or NULL if nothing was found.
 */
static char *collect_issue_text(const char *search_path, size_t *out_len)
{
	FILE *mem;
	struct stat sb;
	char *buf = NULL;
	size_t len = 0;
	char *dup, *saveptr, *entry;
	struct issue_parser parser = {.limit = ISSUE_MAX_SIZE, .n_parsed_files = 0};

	if (!search_path || !*search_path)
		return NULL;

	mem = open_memstream(&buf, &len);
	if (!mem)
		return NULL;

	dup = strdup(search_path);
	if (!dup) {
		fclose(mem);
		return NULL;
	}

	for (entry = strtok_r(dup, ":", &saveptr); entry && parser.limit;
	     entry = strtok_r(NULL, ":", &saveptr)) {
		if (stat(entry, &sb) != 0)
			continue;
		if (S_ISDIR(sb.st_mode))
			append_dir(mem, entry, &parser);
		else
			append_file(mem, entry, &parser);
	}
	free_parser(&parser);
	free(dup);
	fclose(mem);

	if (!len) {
		free(buf);
		return NULL;
	}

	*out_len = len;
	return buf;
}

static bool get_parameter(const char **ppos, const char *end, char *parameter, size_t len)
{
	const char *pos = *ppos;
	size_t l = 0;

	if (!len)
		return false;

	parameter[0] = '\0';
	if (pos >= end || *pos != '{')
		return false;
	pos++;
	while (pos < end && *pos != '}') {
		if (l < len - 1)
			parameter[l++] = *pos;
		pos++;
	}
	parameter[l] = '\0';
	if (pos < end)
		pos++;
	*ppos = pos;
	return true;
}

static void expand_os_release(const char **ppos, const char *end, FILE *out)
{
	char field_name[64];
	char *field_val;

	if (!get_parameter(ppos, end, field_name, sizeof(field_name)))
		field_val = read_os_release_field("PRETTY_NAME");
	else
		field_val = read_os_release_field(field_name);

	if (field_val) {
		fputs(field_val, out);
		free(field_val);
	}
}

/* Write literal text to out, converting \n to \r\n for the terminal. */
static void write_literal(FILE *out, const char *buf, size_t len)
{
	const char *pos = buf, *end = buf + len, *nl;

	while (pos < end) {
		nl = memchr(pos, '\n', end - pos);
		if (!nl) {
			fwrite(pos, 1, end - pos, out);
			break;
		}
		fwrite(pos, 1, nl - pos, out);
		fputs("\r\n", out);
		pos = nl + 1;
	}
}

struct ul_color_name {
	const char *name;
	const char *seq;
};

/*
 * qsort/bsearch buddy
 */
static int cmp_color_name(const void *a0, const void *b0)
{
	const struct ul_color_name *a = (const struct ul_color_name *)a0,
				   *b = (const struct ul_color_name *)b0;
	return strcmp(a->name, b->name);
}

/*
 * Maintains human readable color names
 */
const char *color_sequence_from_colorname(const char *str)
{
	static const struct ul_color_name basic_schemes[] = {
		{"black", UL_COLOR_BLACK},
		{"blink", UL_COLOR_BLINK},
		{"blue", UL_COLOR_BLUE},
		{"bold", UL_COLOR_BOLD},
		{"brown", UL_COLOR_BROWN},
		{"cyan", UL_COLOR_CYAN},
		{"darkgray", UL_COLOR_DARK_GRAY},
		{"gray", UL_COLOR_GRAY},
		{"green", UL_COLOR_GREEN},
		{"halfbright", UL_COLOR_HALFBRIGHT},
		{"lightblue", UL_COLOR_BOLD_BLUE},
		{"lightcyan", UL_COLOR_BOLD_CYAN},
		{"lightgray", UL_COLOR_GRAY},
		{"lightgreen", UL_COLOR_BOLD_GREEN},
		{"lightmagenta", UL_COLOR_BOLD_MAGENTA},
		{"lightred", UL_COLOR_BOLD_RED},
		{"magenta", UL_COLOR_MAGENTA},
		{"red", UL_COLOR_RED},
		{"reset", UL_COLOR_RESET},
		{"reverse", UL_COLOR_REVERSE},
		{"white", UL_COLOR_WHITE},
		{"yellow", UL_COLOR_BOLD_YELLOW}};
	struct ul_color_name key = {.name = str};
	const struct ul_color_name *res;

	if (!str)
		return NULL;

	res = bsearch(&key, basic_schemes, ARRAY_SIZE(basic_schemes), sizeof(struct ul_color_name),
		      cmp_color_name);
	return res ? res->seq : NULL;
}

static void expand_color(const char **ppos, const char *end, FILE *out)
{
	const char *color_escape = NULL;
	char color_name[16]; // longest color name is 13 characters

	fputs("\033", out);

	if (!get_parameter(ppos, end, color_name, sizeof(color_name)))
		return;

	color_escape = color_sequence_from_colorname(color_name);
	if (color_escape)
		fputs(color_escape, out);
}

static void expand_ip(const char **ppos, const char *end, FILE *out, struct addr_book *book,
		      bool ipv6)
{
	char interface_name[IFNAMSIZ];
	const char *address;

	get_parameter(ppos, end, interface_name, sizeof(interface_name));
	if (!book)
		return;

	address = issue_network_get_best_ip(book, interface_name, ipv6);
	if (address)
		fputs(address, out);
}

static void expand_all_ip(const char **ppos, const char *end, FILE *out, struct addr_book *book,
			  bool filter)
{
	char *text;

	if (!book)
		return;

	text = issue_network_get_all_ip(book, filter);
	if (text)
		fputs(text, out);
	free(text);
}

static char *expand_issue(const char *raw, size_t raw_len, char *pty_name, size_t *out_len)
{
	FILE *out;
	char *obuf = NULL;
	size_t olen = 0;
	struct utsname uts;
	struct tm *tm;
	time_t now;
	const char *pos, *end, *esc;
	char timebuf[64];
	char datebuf[64];
	const char *tty_short;
	char code;
	struct addr_book *book = NULL;

	out = open_memstream(&obuf, &olen);
	if (!out)
		return NULL;

	if (uname(&uts) < 0)
		memset(&uts, 0, sizeof(uts));

	now = time(NULL);
	tm = localtime(&now);
	if (tm) {
		strftime(timebuf, sizeof(timebuf), "%H:%M:%S", tm);
		strftime(datebuf, sizeof(datebuf), "%Y-%m-%d", tm);
	} else {
		timebuf[0] = '\0';
		datebuf[0] = '\0';
	}

	if (!strncmp(pty_name, "/dev/", 5))
		tty_short = pty_name + 5;
	else
		tty_short = pty_name;

	pos = raw;
	end = raw + raw_len;
	while (pos < end) {
		esc = memchr(pos, '\\', end - pos);
		if (!esc) {
			write_literal(out, pos, end - pos);
			break;
		}

		write_literal(out, pos, esc - pos);
		pos = esc + 1;
		if (pos >= end)
			break;

		code = *pos++;
		switch (code) {
		case '\\':
			fputc('\\', out);
			break;
		case 's':
			fputs(uts.sysname, out);
			break;
		case 'n':
			fputs(uts.nodename, out);
			break;
		case 'r':
			fputs(uts.release, out);
			break;
		case 'v':
			fputs(uts.version, out);
			break;
		case 'm':
			fputs(uts.machine, out);
			break;
		case 'o':
			fputs(uts.domainname[0] ? uts.domainname : "(none)", out);
			break;
		case 'd':
			fputs(datebuf, out);
			break;
		case 't':
			fputs(timebuf, out);
			break;
		case 'l':
			fputs(tty_short, out);
			break;
		case 'S':
			expand_os_release(&pos, end, out);
			break;
		case 'e':
			expand_color(&pos, end, out);
			break;
		case '4':
			if (!book)
				book = issue_network_gen_book();
			expand_ip(&pos, end, out, book, false);
			break;
		case '6':
			if (!book)
				book = issue_network_gen_book();
			expand_ip(&pos, end, out, book, true);
			break;
		case 'a':
			if (!book)
				book = issue_network_gen_book();
			expand_all_ip(&pos, end, out, book, true);
			break;
		case 'A':
			if (!book)
				book = issue_network_gen_book();
			expand_all_ip(&pos, end, out, book, false);
			break;
		default:
			break;
		}
	}
	issue_network_free_book(book);
	fclose(out);
	*out_len = olen;
	return obuf;
}

/**
 * kmscon_issue_get_buffer
 * Allocate and return a buffer containing the expanded issue text with the given pty name.
 * The buffer must be freed by the caller.
 */
char *kmscon_issue_get_buffer(const char *search_path, char *pty_name, size_t *len)
{
	char *raw;
	size_t raw_len;
	char *out;
	size_t out_len;

	if (!search_path)
		search_path = ISSUE_DEFAULT_PATH;

	raw = collect_issue_text(search_path, &raw_len);
	if (!raw)
		return NULL;

	out = expand_issue(raw, raw_len, pty_name, &out_len);
	free(raw);
	*len = out_len;
	return out;
}
