#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <time.h>
#include <unistd.h>

#include "ssm.h"

#define BUF_LEN (10 * (sizeof(struct inotify_event) + PATH_MAX + 1))
#define MAX_LINE_LEN 4096
#define SECONDS_PER_DAY 86400
#define NOTIFICATION_THRESHOLD 300 /* 5 minutes */

char *get_database_path(void)
{
	char *db = DATABASE_PATH;
	char *db_path;
	if (db[0] == '~') {
		char *home = getenv("HOME");
		if (home == NULL) {
			fprintf(stderr, "$HOME not defined\n");
			return NULL;
		}
		db_path = malloc((strlen(db) + strlen(home)) * sizeof(char));
		if (db_path == NULL) {
			perror("malloc");
			exit(EXIT_FAILURE);
		}

		/* replace ~ with home */
		snprintf(db_path, strlen(db) + strlen(home), "%s%s", home, db + 1);
	} else {
		db_path = strdup(db);
	}
	return db_path;
}


/*
 * Convert time_t into heap-allocated string
 */
char *convert_timestamp(time_t timestamp, datefmt format)
{
	struct tm *time_info = localtime(&timestamp);
	char *time_buf = malloc(20 * sizeof(char));
	if (time_buf == NULL) {
		perror("malloc");
		exit(EXIT_FAILURE);
	}
	switch(format) {
		case FULL:
			strftime(time_buf, 20, "%Y-%m-%d %H:%M:%S", time_info);
			break;
		case HHMM:
			strftime(time_buf, 6, "%H:%M", time_info);
			break;
		default:
			fprintf(stderr, "Invalid datefmt\n");
	}
	return time_buf;
}

void load_events(event **events, int *num_events)
{
	char *db_path = get_database_path();
	if (db_path == NULL)
		return;
	FILE *file = fopen(db_path, "r");
	if (!file) {
		perror("fopen");
		exit(EXIT_FAILURE);
	}

	char line[MAX_LINE_LEN];
	*num_events = 0;
	while (fgets(line, sizeof(line), file)) {
		(*num_events)++;
	}

	*events = malloc(sizeof(event) * (*num_events));
	if (*events == NULL) {
		perror("malloc");
		exit(EXIT_FAILURE);
	}

	fseek(file, 0, SEEK_SET);
	for (int i = 0; fgets(line, sizeof(line), file); i++) {
		sscanf(line, "%ld\t%49[^\t]\t%99[^\t]\t%d\t%d\t%d\t%ld\n",
				&(*events)[i].timestamp, (*events)[i].name, (*events)[i].description,
				&(*events)[i].priority, &(*events)[i].recurrence, &(*events)[i].recurrence_interval,
				&(*events)[i].recurrence_end);
		(*events)[i].notified = 0;
	}

	fclose(file);
	free(db_path);
}

void add_event(time_t timestamp, const char *name,
              const char *description, priority_type priority,
              recurrence_type recurrence, int recurrence_interval,
              time_t recurrence_end)
{
	char *db_path = get_database_path();
	FILE *file;
	file = fopen(db_path, access(db_path, F_OK) ? "w" : "a");
	if (!file) {
		perror("fopen");
		exit(EXIT_FAILURE);
	}

	fprintf(file, "%ld\t%s\t%s\t%d\t%d\t%d\t%ld\n", timestamp, name, description, priority, recurrence, recurrence_interval, recurrence_end);
	fclose(file);
	free(db_path);
	char *time_buf = convert_timestamp(timestamp, 1);
	if (recurrence != NONE) {
		char *end_buf = convert_timestamp(recurrence_end, 1);
		printf("Added \"%s\" with description \"%s\" at \"%s\" (recurs every %d %s until %s)\n", name, description, time_buf, recurrence_interval,
				recurrence == DAILY ? "day" :
				recurrence == WEEKLY ? "week" :
				recurrence == MONTHLY ? "month" : "year", end_buf);
		free(end_buf);
		return;
	} else {
		printf("Added \"%s\" with description \"%s\" at \"%s\"\n", name, description, time_buf);
	}
	free(time_buf);
}

static void send_notification(event to_alert)
{
	char *time_buf = convert_timestamp(to_alert.timestamp, 1);
	char description_buf[512];
	snprintf(description_buf, sizeof(description_buf),
			"Event \"%s\" starts at %s - %s", to_alert.name, time_buf,
			to_alert.description);

	free(time_buf);

	int pid = fork();
	if (pid == 0) {
		/* Child */
		execlp(notifier, notifier, "Upcoming event", description_buf, NULL);
		_exit(1);
	} else if (pid > 0) {
		/* Parent */
	} else {
		perror("fork");
	}
}

void check_events(event *events, int *num_events)
{
	time_t now = time(NULL);
	for (int i = 0; i < *num_events; i++) {
		if (!events[i].notified &&
				events[i].timestamp > now &&
				events[i].timestamp - now <= NOTIFICATION_THRESHOLD) {

			send_notification(events[i]);
			events[i].notified = 1;
		}
	}
}

static char *get_relative_day(time_t event_time)
{
	time_t now = time(NULL);
	int days = (event_time - now) / SECONDS_PER_DAY;
	static char buffer[32];

	if (days == 0) return "today";
	else if (days == 1) return "tomorrow";
	else sprintf(buffer, "in %d days", days);
	return buffer;
}

int is_today(time_t t)
{
	time_t now = time(NULL);
	struct tm *time_tm = localtime(&t);
	struct tm *now_tm = localtime(&now);
	return (time_tm->tm_year == now_tm->tm_year &&
			time_tm->tm_mon == now_tm->tm_mon &&
			time_tm->tm_mday == now_tm->tm_mday);
}

void print_event(event e, int relative)
{
	char *time_str = convert_timestamp(e.timestamp, relative ? FULL : HHMM);
	if (relative) {
		char *relative_day = get_relative_day(e.timestamp);
		printf("%s (%s): %s - %s\n", time_str, relative_day, e.name, e.description);
	} else {
		printf("%s: %s - %s\n", time_str, e.name, e.description);
	}
	free(time_str);
}

void list_today_events(event *events, int num_events)
{
	printf("\nToday's events:\n");
	printf("---------------\n");
	int found = 0;

	for (int i = 0; i < num_events; i++) {
		if (is_today(events[i].timestamp)) {
			print_event(events[i], 0);
			found = 1;
		}
	}

	if (!found) {
		printf("No events scheduled for today\n");
	}
}

void list_upcoming_events(event *events, int num_events)
{
	printf("\nUpcoming events (next 28 days):\n");
	printf("------------------------------\n");
	time_t now = time(NULL) + SECONDS_PER_DAY;
	time_t week_later = now + (28 * SECONDS_PER_DAY);
	int found = 0;

	for (int i = 0; i < num_events; i++) {
		if (events[i].timestamp > now && events[i].timestamp <= week_later) {
			print_event(events[i], 1);
			found = 1;
		}
	}

	if (!found) {
		printf("No upcoming events in the next 7 days\n");
	}
}

void expand_recurring_events(event **events, int *num_events)
{
	int new_count = *num_events;
	time_t now = time(NULL);

	/* Count how many new events we'll need */
	for (int i = 0; i < *num_events; i++) {
		if ((*events)[i].recurrence != NONE) {
			time_t next_occurrence = (*events)[i].timestamp;
			while (next_occurrence <= (*events)[i].recurrence_end) {
				if (next_occurrence >= now) {
					new_count++;
				}

				/* Calculate next occurrence based on recurrence type */
				switch ((*events)[i].recurrence) {
					case DAILY:
						next_occurrence += SECONDS_PER_DAY * (*events)[i].recurrence_interval;
						break;
					case WEEKLY:
						next_occurrence += SECONDS_PER_DAY * 7 * (*events)[i].recurrence_interval;
						break;
					case MONTHLY:
						next_occurrence += SECONDS_PER_DAY * 30 * (*events)[i].recurrence_interval;
						break;
					case YEARLY:
						next_occurrence += SECONDS_PER_DAY * 365 * (*events)[i].recurrence_interval;
						break;
					default:
						break;
				}
			}
		}
	}

	event *new_events = malloc(sizeof(event) * new_count);
	if (!new_events) {
		perror("malloc");
		exit(EXIT_FAILURE);
	}

	/* Copy original events and expand recurring ones */
	int current_index = 0;
	for (int i = 0; i < *num_events; i++) {
		if ((*events)[i].recurrence != NONE) {
			time_t next_occurrence = (*events)[i].timestamp;
			while (next_occurrence <= (*events)[i].recurrence_end) {
				if (next_occurrence >= now) {
					event new_event = (*events)[i];
					new_event.timestamp = next_occurrence;
					memcpy(&new_events[current_index++], &new_event, sizeof(event));
				}

				switch ((*events)[i].recurrence) {
					case DAILY:
						next_occurrence += SECONDS_PER_DAY * (*events)[i].recurrence_interval;
						break;
					case WEEKLY:
						next_occurrence += SECONDS_PER_DAY * 7 * (*events)[i].recurrence_interval;
						break;
					case MONTHLY:
						next_occurrence += SECONDS_PER_DAY * 30 * (*events)[i].recurrence_interval;
						break;
					case YEARLY:
						next_occurrence += SECONDS_PER_DAY * 365 * (*events)[i].recurrence_interval;
						break;
					default:
						break;
				}
			}
		} else {
			memcpy(&new_events[current_index++], &(*events)[i], sizeof(event));
		}
	}

	*events = new_events;
	*num_events = new_count;
}

static int compare_by_start_time(const void *a, const void *b)
{
	return ((event *) a)->timestamp - ((event *) b)->timestamp;
}

static int compare_by_priority(const void *a, const void *b)
{
	return ((event *) b)->priority - ((event *) a)->priority;
}

void sort_events(event *events, int num_events, int sort_type)
{
	switch (sort_type) {
		/* Start time */
		case 0:
			qsort(events, num_events, sizeof(event), compare_by_start_time);
			break;
			/* priority_type (NOT IMPLEMENTED) */
		case 1:
			qsort(events, num_events, sizeof(event), compare_by_priority);
			break;
	}
}

void watch_file(event *events, int *num_events)
{
	load_events(&events, num_events);
	expand_recurring_events(&events, num_events);
	while (1) {
		// daemon to show notifications
		check_events(events, num_events);
	}
	
	free(events);
}

parsed_time parse_relative_time(const char *time_str)
{
	parsed_time result = {0, 0};
	char *str = strdup(time_str);
	char *number = str;
	char *unit = str;

	/* Find the separation between number and unit */
	while (*unit && isdigit(*unit)) unit++;
	if (*unit) {
		*unit = '\0';
		unit++;
	}

	if (!*number || !*unit) {
		free(str);
		return result;
	}

	int value = atoi(number);
	time_t now = time(NULL);

	if (strcmp(unit, "min") == 0 || strcmp(unit, "mins") == 0) {
		result.timestamp = now + (value * 60);
	} else if (strcmp(unit, "hour") == 0 || strcmp(unit, "hours") == 0) {
		result.timestamp = now + (value * 3600);
	} else if (strcmp(unit, "day") == 0 || strcmp(unit, "days") == 0) {
		result.timestamp = now + (value * 86400);
	} else if (strcmp(unit, "week") == 0 || strcmp(unit, "weeks") == 0) {
		result.timestamp = now + (value * 86400 * 7);
	} else {
		free(str);
		return result;
	}

	result.is_valid = 1;
	free(str);
	return result;
}

parsed_time parse_absolute_time(const char *time_str)
{
	parsed_time result = {0, 0};
	struct tm tm = {0};
	char *formats[] = {
		"%Y-%m-%d %H:%M:%S",
		"%Y-%m-%d %H:%M",
		"%Y/%m/%d %H:%M:%S",
		"%Y/%m/%d %H:%M",
		"%d-%m-%Y %H:%M:%S",
		"%d-%m-%Y %H:%M",
		"%d/%m/%Y %H:%M:%S",
		"%d/%m/%Y %H:%M",
		"%d-%m-%Y",
		"%Y-%m-%d",
		"%d/%m/%Y",
		"%Y/%m/%d",
		"%H:%M",
	};

	for (int i = 0; i < sizeof(formats)/sizeof(formats[0]); i++) {
		char *parsed = strptime(time_str, formats[i], &tm);
		if (parsed != NULL) {
			/* Use today's date */
			if (strcmp(formats[i], "%H:%M") == 0) {
				time_t now = time(NULL);
				struct tm *today = localtime(&now);
				tm.tm_year = today->tm_year;
				tm.tm_mon = today->tm_mon;
				tm.tm_mday = today->tm_mday;
			}

			tm.tm_isdst = -1;
			result.timestamp = mktime(&tm);
			result.is_valid = 1;
			return result;
		}
	}

	return result;
}

parsed_time parse_time_string(const char *time_str)
{
	/* Parsing as relative time first */
	parsed_time result = parse_relative_time(time_str);
	if (result.is_valid) return result;

	/* Try absolute time */
	return parse_absolute_time(time_str);
}

static _Noreturn void usage(int code)
{
	fprintf(code ? stderr : stdout,
			"Simple Scheduler Manager " VERSION "\n\n"
			"Usage: ssm <command>\n\n"
			"	help						Show this help message\n"
			"	sched <time> <title> <description> [options]	Schedule an event\n"
			"	edit						Edit schedule with $EDITOR\n"
			"	list						List all upcoming events\n"
			"	run						Spawn notifier daemon\n"
		   );
	exit(code);
}

int main(int argc, char **argv)
{
	event *events = NULL;
	int num_events = 0;
	if (argc == 1 || (argc == 2 && !strncmp(argv[1], "list", 4))) {
		/* Load and expand recurring events */
		char *time_buf = convert_timestamp(time(NULL), 0);
		printf("Current date: %s\n", time_buf);
		free(time_buf);

		load_events(&events, &num_events);
		list_today_events(events, num_events);

		expand_recurring_events(&events, &num_events);

		/* Sort events by start time */
		sort_events(events, num_events, 0);
		list_upcoming_events(events, num_events);
		return EXIT_SUCCESS;
	}
	if (strcmp(argv[1], "sched") == 0) {
		if (argc < 4) {
			fprintf(stderr, "Usage: ssm sched <time> <title> <description> [options]\n");
			fprintf(stderr, "Options:\n");
			fprintf(stderr, "  --priority <low|medium|high>\n");
			fprintf(stderr, "  --recur <daily|weekly|monthly|yearly>\n");
			fprintf(stderr, "  --interval <number>\n");
			fprintf(stderr, "  --until <end-date>\n");
			fprintf(stderr, "\nTime formats:\n");
			fprintf(stderr, "  Relative: 30 min, 2 hours, 1 day, 2 weeks\n");
			fprintf(stderr, "  Absolute: YYYY-MM-DD DD-MM-YY [HH:MM:SS] HH:MM[:SS]\n");
			return EXIT_FAILURE;
		}

		char *when = argv[2];
		char *name = argv[3];
		char *description = argv[4];

		parsed_time pt = parse_time_string(when);
		if (!pt.is_valid) {
			fprintf(stderr, "Invalid time format: %s\n", when);
			return EXIT_FAILURE;
		}

		priority_type priority = MEDIUM;
		recurrence_type recurrence = NONE;
		int recurrence_interval = 1;
		time_t recurrence_end = 0;

		/* Parse optional arguments */
		for (int i = 5; i < argc; i++) {
			if (!strcmp(argv[i], "--priority") && i + 1 < argc) {
				if (!strcmp(argv[i + 1], "low")) priority = LOW;
				else if (!strcmp(argv[i + 1], "high")) priority = HIGH;
				i++;
			} else if (!strcmp(argv[i], "--recur") && i + 1 < argc) {
				if (!strcmp(argv[i + 1], "daily")) recurrence = DAILY;
				else if (!strcmp(argv[i + 1], "weekly")) recurrence = WEEKLY;
				else if (!strcmp(argv[i + 1], "monthly")) recurrence = MONTHLY;
				else if (!strcmp(argv[i + 1], "yearly")) recurrence = YEARLY;
				i++;
			} else if (!strcmp(argv[i], "--interval") && i + 1 < argc) {
				recurrence_interval = atoi(argv[i + 1]);
				i++;
			} else if (!strcmp(argv[i], "--until") && i + 1 < argc) {
				parsed_time end_time = parse_time_string(argv[i + 1]);
				if (end_time.is_valid) {
					recurrence_end = end_time.timestamp;
				} else {
					fprintf(stderr, "Invalid end date format: %s\n", argv[i + 1]);
					return EXIT_FAILURE;
				}
				i++;
			}
		}

		add_event(pt.timestamp, name, description, priority, recurrence,
				recurrence_interval, recurrence_end);

		char *time_str = convert_timestamp(pt.timestamp, 0);
		printf("Time: %s\n", time_str);
		printf("Title: %s\n", name);
		printf("Description: %s\n", description);
		printf("Priority: %s\n", priority == LOW ? "Low" : (priority == HIGH ? "High" : "Medium"));
		if (recurrence != NONE) {
			printf("Recurrence: %s (every %d ",
					recurrence == DAILY ? "Daily" :
					recurrence == WEEKLY ? "Weekly" :
					recurrence == MONTHLY ? "Monthly" : "Yearly",
					recurrence_interval);
			printf("%s)\n",
					recurrence == DAILY ? "days" :
					recurrence == WEEKLY ? "weeks" :
					recurrence == MONTHLY ? "months" : "years");
			if (recurrence_end) {
				char *end_str = convert_timestamp(recurrence_end, 0);
				printf("Until: %s\n", end_str);
				free(end_str);
			}
		}
		free(time_str);
	} else if (strcmp(argv[1], "edit") == 0) {
		const char *e = getenv("EDITOR");
		if (e == NULL) {
			e = editor;
			fprintf(stderr, "$EDITOR not defined, falling back to %s\n", e);
		}
		char *db_path = get_database_path();
		if (!db_path) {
			fprintf(stderr, "Cannot find database path\n");
			return EXIT_FAILURE;
		}
		execlp(e, e, db_path, NULL);
		perror("Failed to spawn editor");
		return EXIT_FAILURE;
	} else if (strcmp(argv[1], "run") == 0) {
		watch_file(events, &num_events);
	} else if (strcmp(argv[1], "help") == 0) {
		usage(0);
	} else {
		fprintf(stderr, "Unknown command: %s\n", argv[1]);
		usage(1);
	}
	return EXIT_SUCCESS;
}