/*
    cssh - cluster secure shell
    Copyright (C) 2015-2026  Andreas Baumann

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "copy_common.h"

#include <dirent.h>
#include <errno.h>
#include <inttypes.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

void create_dir_stack( dir_stack_t *stack )
{
	stack->head = NULL;
}

int push_dir_stack( dir_stack_t *stack, const char *dir )
{
	struct dir_stack_entry_t *e = (struct dir_stack_entry_t *)malloc( sizeof( dir_stack_entry_t ) );
	if( e == NULL ) {
		return -1;
	}

	e->dir = strdup( dir );
	if( e->dir == NULL ) {
		free( e );
		return -1;
	}
	e->next = stack->head;
	stack->head = e;

	return 0;
}

void pop_dir_stack( dir_stack_t *stack )
{
	if( stack->head != NULL ) {
		struct dir_stack_entry_t *e = stack->head;
		stack->head = stack->head->next;
		free( e->dir );
		free( e );
	}
}

char *top_dir_stack( dir_stack_t *stack )
{
	if( stack->head != NULL ) {
		return stack->head->dir;
	} else {
		return NULL;
	}
}

void free_dir_stack( dir_stack_t *stack )
{
	while( stack->head != NULL ) {
		pop_dir_stack( stack );
	}
}

char *join_path( const char *dir, const char *name )
{
	size_t len = strlen( dir ) + 1 + strlen( name ) + 1;
	char *full = (char *)malloc( len );
	if( full == NULL ) {
		return NULL;
	}
	snprintf( full, len, "%s/%s", dir, name );
	return full;
}

bool is_directory( const char *path )
{
	struct stat st;
	if( stat( path, &st ) < 0 ) {
		return false;
	}
	return S_ISDIR( st.st_mode );
}

char *path_basename( const char *path )
{
	char *tmp = strdup( path );
	if( tmp == NULL ) {
		return NULL;
	}
	char *b = strdup( basename( tmp ) );
	free( tmp );
	return b;
}

typedef struct local_walk_dir_frame_t {
	DIR *dh;
	char *path;
	struct local_walk_dir_frame_t *next;
} local_walk_dir_frame_t;

struct local_walk_t {
	bool recursive;
	bool root_pending;
	bool finished;
	char *root_path;
	char *root_name;
	local_walk_dir_frame_t *stack;
};

local_walk_t *walk_local_tree_open( const char *root_path, bool recursive )
{
	local_walk_t *w = (local_walk_t *)calloc( 1, sizeof( local_walk_t ) );
	if( w == NULL ) {
		return NULL;
	}

	w->root_path = strdup( root_path );
	w->root_name = path_basename( root_path );
	if( w->root_path == NULL || w->root_name == NULL ) {
		free( w->root_path );
		free( w->root_name );
		free( w );
		return NULL;
	}

	w->recursive = recursive;
	w->root_pending = true;
	w->finished = false;
	w->stack = NULL;

	return w;
}

static void push_walk_frame( local_walk_t *w, DIR *dh, char *path )
{
	local_walk_dir_frame_t *f = (local_walk_dir_frame_t *)malloc( sizeof( local_walk_dir_frame_t ) );
	if( f == NULL ) {
		closedir( dh );
		free( path );
		return;
	}
	f->dh = dh;
	f->path = path;
	f->next = w->stack;
	w->stack = f;
}

static void pop_walk_frame( local_walk_t *w )
{
	local_walk_dir_frame_t *top = w->stack;
	w->stack = top->next;
	closedir( top->dh );
	free( top->path );
	free( top );
}

int walk_local_tree_next( local_walk_t *w, walk_event_t *ev )
{
	memset( ev, 0, sizeof( *ev ) );

	if( w->finished ) {
		return 0;
	}

	if( w->root_pending ) {
		w->root_pending = false;

		struct stat st;
		if( lstat( w->root_path, &st ) < 0 ) {
			fprintf( stderr, "ERROR: failed to stat '%s': %s\n", w->root_path, strerror( errno ) );
			return -1;
		}

		if( S_ISLNK( st.st_mode ) ) {
			fprintf( stderr, "WARNING: skipping symlink '%s'\n", w->root_path );
			w->finished = true;
			return 0;
		} else if( S_ISDIR( st.st_mode ) ) {
			if( !w->recursive ) {
				fprintf( stderr, "ERROR: omitting directory '%s' (use -r for recursive copy)\n", w->root_path );
				return -1;
			}
			DIR *dh = opendir( w->root_path );
			if( dh == NULL ) {
				fprintf( stderr, "ERROR: failed to open directory '%s': %s\n", w->root_path, strerror( errno ) );
				return -1;
			}
			push_walk_frame( w, dh, strdup( w->root_path ) );
			ev->type = CSSH_WALK_ENTER_DIR;
			ev->name = strdup( w->root_name );
			ev->full_path = strdup( w->root_path );
			ev->mode = st.st_mode & 07777;
			return 1;
		} else if( S_ISREG( st.st_mode ) ) {
			ev->type = CSSH_WALK_FILE;
			ev->name = strdup( w->root_name );
			ev->full_path = strdup( w->root_path );
			ev->mode = st.st_mode & 07777;
			ev->size = (uint64_t)st.st_size;
			w->finished = true;
			return 1;
		} else {
			fprintf( stderr, "WARNING: skipping special file '%s'\n", w->root_path );
			w->finished = true;
			return 0;
		}
	}

	while( w->stack != NULL ) {
		errno = 0;
		struct dirent *de = readdir( w->stack->dh );
		if( de == NULL ) {
			ev->type = CSSH_WALK_LEAVE_DIR;
			ev->full_path = strdup( w->stack->path );
			pop_walk_frame( w );
			if( w->stack == NULL ) {
				w->finished = true;
			}
			return 1;
		}

		if( strcmp( de->d_name, "." ) == 0 || strcmp( de->d_name, ".." ) == 0 ) {
			continue;
		}

		char *full = join_path( w->stack->path, de->d_name );
		if( full == NULL ) {
			fprintf( stderr, "ERROR: memory allocation failed for path of '%s'\n", de->d_name );
			return -1;
		}

		struct stat st;
		if( lstat( full, &st ) < 0 ) {
			fprintf( stderr, "WARNING: failed to stat '%s': %s\n", full, strerror( errno ) );
			free( full );
			continue;
		}

		if( S_ISLNK( st.st_mode ) ) {
			fprintf( stderr, "WARNING: skipping symlink '%s'\n", full );
			free( full );
			continue;
		} else if( S_ISDIR( st.st_mode ) ) {
			DIR *dh = opendir( full );
			if( dh == NULL ) {
				fprintf( stderr, "WARNING: failed to open directory '%s': %s\n", full, strerror( errno ) );
				free( full );
				continue;
			}
			ev->type = CSSH_WALK_ENTER_DIR;
			ev->name = strdup( de->d_name );
			ev->full_path = strdup( full );
			ev->mode = st.st_mode & 07777;
			push_walk_frame( w, dh, full );
			return 1;
		} else if( S_ISREG( st.st_mode ) ) {
			ev->type = CSSH_WALK_FILE;
			ev->name = strdup( de->d_name );
			ev->mode = st.st_mode & 07777;
			ev->size = (uint64_t)st.st_size;
			ev->full_path = full;
			return 1;
		} else {
			fprintf( stderr, "WARNING: skipping special file '%s'\n", full );
			free( full );
			continue;
		}
	}

	w->finished = true;
	return 0;
}

void free_walk_event( walk_event_t *ev )
{
	free( ev->name );
	free( ev->full_path );
	ev->name = NULL;
	ev->full_path = NULL;
}

void walk_local_tree_close( local_walk_t *w )
{
	if( w == NULL ) {
		return;
	}
	while( w->stack != NULL ) {
		pop_walk_frame( w );
	}
	free( w->root_path );
	free( w->root_name );
	free( w );
}

static void format_size( uint64_t bytes, char *buf, size_t bufsize )
{
	static const char *units[] = { "B", "KB", "MB", "GB", "TB" };
	double value = (double)bytes;
	size_t unit = 0;

	while( value >= 1024.0 && unit < ( sizeof( units ) / sizeof( units[0] ) ) - 1 ) {
		value /= 1024.0;
		unit++;
	}

	if( unit == 0 ) {
		snprintf( buf, bufsize, "%"PRIu64" %s", bytes, units[unit] );
	} else {
		snprintf( buf, bufsize, "%.1f %s", value, units[unit] );
	}
}

void print_transfer_done( const char *host, const char *path, uint64_t size )
{
	char sizebuf[32];
	format_size( size, sizebuf, sizeof( sizebuf ) );
	fprintf( stderr, "done: %s (%s) [%s]\n", path, sizebuf, host );
}

int resolve_local_download_root( const char *local_directory )
{
	if( local_directory == NULL || local_directory[0] == '\0' || strcmp( local_directory, "." ) == 0 ) {
		return 0;
	}

	if( mkdir( local_directory, 0750 ) < 0 && errno != EEXIST ) {
		fprintf( stderr, "ERROR: failed to create local destination directory '%s': %s\n",
			local_directory, strerror( errno ) );
		return -1;
	}

	if( chdir( local_directory ) < 0 ) {
		fprintf( stderr, "ERROR: failed to change to local destination directory '%s': %s\n",
			local_directory, strerror( errno ) );
		return -1;
	}

	return 0;
}
