/* SOGoToolRestore.m - this file is part of SOGo
 *
 * Copyright (C) 2009-2020 Inverse inc.
 *
 * This file 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 2, or (at your option)
 * any later version.
 *
 * This file 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSFileManager.h>
#import <Foundation/NSString.h>
#import <Foundation/NSUserDefaults.h>

#import <GDLAccess/EOAdaptorChannel.h>
#import <GDLAccess/EOAdaptorContext.h>

#import <GDLContentStore/GCSChannelManager.h>
#import <GDLContentStore/GCSFolderManager.h>
#import <GDLContentStore/GCSFolder.h>
#import <GDLContentStore/NSURL+GCS.h>

#import <Appointments/iCalEntityObject+SOGo.h>
#import <SOGo/NSArray+Utilities.h>
#import "SOGo/SOGoCredentialsFile.h"
#import <SOGo/SOGoProductLoader.h>
#import <SOGo/SOGoUser.h>
#import <SOGo/SOGoUserManager.h>
#import <SOGo/SOGoUserProfile.h>
#import <SOGo/SOGoUserSettings.h>
#import <SOGo/SOGoSystemDefaults.h>

#import <Mailer/SOGoMailAccounts.h>
#import <Mailer/SOGoMailAccount.h>

#import <NGCards/iCalCalendar.h>
#import <NGCards/NGVList.h>

#import <NGObjWeb/WOContext+SoObjects.h>

#import "SOGoToolRestore.h"

/* TODO:
   - respond to "--help restore"
   - handle database connectivity errors
   - handle the case where the restored folder has been deleted
   - write methods in GDLContentStore to get/update displayname
     and storing roles */

@implementation SOGoToolRestore

+ (NSString *) command
{
  return @"restore";
}

+ (NSString *) description
{
  return @"restore user folders";
}

- (id) init
{
  if ((self = [super init]))
    {
      directory = nil;
      userID = nil;
      filename = nil;
      restoreFolder = nil;
      destructive = NO;
    }

  return self;
}

- (void) dealloc
{
  [directory release];
  [userID release];
  [filename release];
  [restoreFolder release];
  [super dealloc];
}

- (void) usage
{
  fprintf (stderr, "restore [-l|-p|-f/-F folder/ALL] [-c credentialFile] directory user\n\n"
	   "           directory  the directory where backup files were initially stored\n"
	   "           user       the user of whom to restore the data\n"
	   "           -l         flag used to list folders to restore\n"
	   "           -p         flag used to restore only the user's preferences\n"
	   "           -f/-F      flag used to specify which folder to restore, ALL for everything\n"
	   "           -c credentialFile    Specify the file containing the sieve admin credentials\n"
	   "                                If set, it is used automatically to generate the sieve\n"
           "                                script after a data restore.\n"
	   "                                The file should contain a single line:\n"
	   "                                username:password\n\n"
	   "Examples:   sogo-tool restore -l /tmp/foo bob\n"
	   "            sogo-tool restore -f Contacts/personal /tmp/foo bob\n"
	   "            sogo-tool restore -p /tmp/foo bob\n");
}

- (Class) parsingClassForContent: (NSString *) theContent
{
  CardGroup *cardEntry;
  NSString *firstTag;
  Class objectClass;

  objectClass = Nil;

  cardEntry = [CardGroup parseSingleFromSource: theContent];
  if (cardEntry)
    {
      firstTag = [[cardEntry tag] uppercaseString];
      if ([firstTag isEqualToString: @"VCARD"])
	objectClass = [NGVCard class];
      else if ([firstTag isEqualToString: @"VLIST"])
	objectClass = [NGVList class];
      else
	objectClass = [iCalCalendar class];
    }

  return objectClass;
}

- (BOOL) checkDirectory
{
  NSFileManager *fm;
  BOOL exists, isDir, rc;

  fm = [NSFileManager defaultManager];
  exists = [fm fileExistsAtPath: directory isDirectory: &isDir];
  if (exists)
    {
      if (isDir)
        rc = YES;
      else
        {
          rc = NO;
          NSLog (@"specified directory is a regular file");
        }
    }
  else
    {
      rc = NO;
      NSLog (@"specified directory does not exist");
    }

  return rc;
}

- (BOOL) fetchUserID: (NSString *) identifier
{
  SOGoSystemDefaults *sd;
  SOGoUserManager *lm;
  NSDictionary *infos;
  NSString *uid = nil;

  BOOL rc;

  lm = [SOGoUserManager sharedUserManager];
  infos = [lm contactInfosForUserWithUIDorEmail: identifier];
  uid = nil;

  if (infos)
    {
      sd = [SOGoSystemDefaults sharedSystemDefaults];
      uid = [infos objectForKey: @"c_uid"];

      if ([sd enableDomainBasedUID] && [uid rangeOfString: @"@"].location == NSNotFound)
        uid = [NSString stringWithFormat: @"%@@%@",
                     [infos objectForKey: @"c_uid"],
                     [infos objectForKey: @"c_domain"]];

      if ([[infos objectForKey: @"DomainLessLogin"] boolValue])
        ASSIGN(filename, [infos objectForKey: @"c_uid"]);
      else
        ASSIGN(filename, uid);
    }

  ASSIGN (userID, uid);

  if (userID)
    rc = YES;
  else
    {
      rc = NO;
      NSLog (@"user '%@' not found", identifier);
    }

  return rc;
}

- (int) parseModeArguments
{
  NSString *mode;
  int count, max;

  max = [arguments count];
  if (max > 0)
    {
      mode = [arguments objectAtIndex: 0];
      count = 1;
      if ([mode isEqualToString: @"-f"]
          || [mode isEqualToString: @"-F"])
        {
          if ([mode hasSuffix: @"f"])
            restoreMode = SOGoToolRestoreFolderMode;
          else
            restoreMode = SOGoToolRestoreFolderDestructiveMode;
          if (max > 1)
            {
              count++;
              ASSIGN (restoreFolder, [arguments objectAtIndex: 1]);
            }
          else
            {
              count = 0;
              NSLog (@"missing 'folder' parameter");
            }
        }
      else if ([mode isEqualToString: @"-l"])
        restoreMode = SOGoToolRestoreListFoldersMode;
      else if ([mode isEqualToString: @"-p"])
        restoreMode = SOGoToolRestorePreferencesMode;
      else
        {
          count = 0;
          if ([mode hasPrefix: @"-"])
            NSLog (@"specified mode is invalid");
          else
            NSLog (@"missing 'mode' parameter");
        }
    }
  else
    count = 0;

  return count;
}

- (BOOL) parseArguments
{
  BOOL rc;
  NSString *identifier;
  NSArray *newArguments;
  int count, max, v;

  count = [self parseModeArguments];
  max = [arguments count] - count;
  v = 2;

  if ([[NSUserDefaults standardUserDefaults] stringForKey: @"c"])
    {
      count = 3;
      v = 4;
    }

  if (max == v)
    {
      newArguments
        = [arguments subarrayWithRange: NSMakeRange (count, 2)];
      ASSIGN (directory, [newArguments objectAtIndex: 0]);
      identifier = [newArguments objectAtIndex: 1];
      rc = ([self checkDirectory]
            && [self fetchUserID: identifier]);
    }
  else
    {
      [self usage];
      rc = NO;
    }

  return rc;
}

- (BOOL) restoreDisplayName: (NSString *) newDisplayName
                   ofFolder: (GCSFolder *) gcsFolder
                     withFM: (GCSFolderManager *) fm
{
  BOOL rc;
  GCSChannelManager *cm;
  EOAdaptorChannel *fc;
  NSURL *folderLocation;
  NSString *sql;

  if (newDisplayName)
    {
      rc = YES;

      cm = [fm channelManager];
      folderLocation = [fm folderInfoLocation];
      fc = [cm acquireOpenChannelForURL: folderLocation];
      if (fc)
        {
          sql
            = [NSString stringWithFormat: (@"UPDATE %@"
                                           @" SET c_foldername = '%@'"
                                           @" WHERE c_path = '%@'"),
                        [folderLocation gcsTableName],
                        [newDisplayName stringByReplacingString: @"'"
                                                     withString: @"''"],
                        [gcsFolder path]];
          [fc evaluateExpressionX: sql];
          [cm releaseChannel: fc  immediately: YES];
        }
    }
  else
    {
      rc = NO;
      NSLog (@"no display name found (abort)");
    }

  return rc;
}

- (BOOL) restoreACL: (NSDictionary *) acl
           ofFolder: (GCSFolder *) gcsFolder
{
  EOAdaptorChannel *channel;
  NSEnumerator *users, *userRoles;
  NSString *SQL, *folderPath, *aclTableName, *currentUser, *currentRole;
  BOOL rc;

  if (acl)
    {
      aclTableName = [gcsFolder aclTableName];
      folderPath = [[gcsFolder path] substringFromIndex: 6];

      [gcsFolder deleteAclWithSpecification: nil];

      channel = [gcsFolder acquireAclChannel];
      [[channel adaptorContext] beginTransaction];

      users = [[acl allKeys] objectEnumerator];
      while ((currentUser = [users nextObject]))
        {
          userRoles = [[acl objectForKey: currentUser] objectEnumerator];
          while ((currentRole = [userRoles nextObject]))
            {
              if ([GCSFolderManager singleStoreMode])
                SQL = [NSString stringWithFormat: @"INSERT INTO %@"
                                @" (c_object, c_uid, c_role, c_folder_id)"
                                @" VALUES ('%@', '%@', '%@', %@)",
                                aclTableName,
                                folderPath, currentUser, currentRole, [gcsFolder folderId]];
              else
                SQL = [NSString stringWithFormat: @"INSERT INTO %@"
                                @" (c_object, c_uid, c_role)"
                                @" VALUES ('%@', '%@', '%@')",
                                aclTableName,
                                folderPath, currentUser, currentRole];
              [channel evaluateExpressionX: SQL];
            }
        }

      [[channel adaptorContext] commitTransaction];
      [gcsFolder releaseChannel: channel  immediately: YES];

      rc = YES;
    }
  else
    {
      rc = NO;
      NSLog (@"no acl found (abort)");
    }

  return rc;
}

- (NSDictionary *) fetchExistingRecordsFromFolder: (GCSFolder *) gcsFolder
{
  NSArray *records;
  int count, max;
  NSDictionary *row;
  NSMutableDictionary *existingRecords;

  records = [gcsFolder fetchFields: [NSArray arrayWithObject: @"c_name"]
                fetchSpecification: nil];
  max = [records count];
  existingRecords = [NSMutableDictionary dictionaryWithCapacity: max];
  for (count = 0; count < max; count++)
    {
      row = [records objectAtIndex: count];
      [existingRecords setObject: @""
                          forKey: [row objectForKey: @"c_name"]];
    }

  return existingRecords;
}

- (BOOL) restoreRecords: (NSArray *) records
               ofFolder: (GCSFolder *) gcsFolder
{
  NSDictionary *existingRecords, *currentRecord;
  NSString *cName, *cContent;
  NSAutoreleasePool *pool;

  unsigned int version;
  int count, max;
  BOOL rc;

  if (records)
    {
      existingRecords = [self fetchExistingRecordsFromFolder: gcsFolder];
      pool = [[NSAutoreleasePool alloc] init];
      version = 0;
      rc = YES;
      max = [records count];
      for (count = 0; count < max; count++)
        {
          version = 0;
          if (count > 0 && count%100 == 0)
            {
              DESTROY(pool);
              pool = [[NSAutoreleasePool alloc] init];
            }
          currentRecord = [records objectAtIndex: count];
          cName = [currentRecord objectForKey: @"c_name"];
          if (![existingRecords objectForKey: cName])
            {
              NSLog (@"restoring record '%@' with version %d", cName, version);
              cContent = [currentRecord objectForKey: @"c_content"];
              [gcsFolder writeContent: cContent
                        fromComponent: [[self parsingClassForContent: cContent] parseSingleFromSource: cContent]
                            container: nil
                               toName: cName
                          baseVersion: &version];
            }
        }
      DESTROY(pool);
    }
  else
    {
      rc = NO;
      NSLog (@"no records found (abort)");
    }

  return rc;
}

- (BOOL) createFolder: (NSString *) folder
               withFM: (GCSFolderManager *) fm
{
  NSArray *pathElements;
  NSException *error;
  NSString *folderType;
  BOOL rc;

  pathElements = [folder componentsSeparatedByString: @"/"];
  if ([[pathElements objectAtIndex: 3] isEqualToString: @"Contacts"])
    folderType = @"Contact";
  else
    folderType = @"Appointment";

  error = [fm createFolderOfType: folderType
                        withName: [pathElements objectAtIndex: 4]
                          atPath: folder];
  if (error)
    {
      rc = NO;
      NSLog (@"an error occured during folder creation: %@", error);
    }
  else
    rc = YES;

  return rc;
}

- (BOOL) restoreFolder: (NSString *) folder
           withContent: (NSDictionary *) content
           destructive: (BOOL) isDestructive
{
  GCSFolderManager *fm;
  GCSFolder *gcsFolder;
  NSException *error;
  BOOL rc;

  rc = YES;

  fm = [GCSFolderManager defaultFolderManager];
  gcsFolder = [fm folderAtPath: folder];
  if (gcsFolder && isDestructive)
    {
      error = [fm deleteFolderAtPath: folder];
      if (error)
        {
          rc = NO;
          NSLog (@"an error occured during folder deletion: %@", error);
        }
      else
        gcsFolder = nil;
    }
  if (rc)
    {
      if (!gcsFolder)
        {
          rc = [self createFolder: folder withFM: fm];
          if (rc)
            {
              gcsFolder = [fm folderAtPath: folder];
              if (!gcsFolder)
                {
                  rc = NO;
                  NSLog (@"missing folder '%@' could not be recreated",
                         folder);
                }
            }
        }

      rc &= ([self restoreDisplayName: [content objectForKey: @"displayname"]
                             ofFolder: gcsFolder
                               withFM: fm]
             && [self restoreACL: [content objectForKey: @"acl"]
                        ofFolder: gcsFolder]
             && [self restoreRecords: [content objectForKey: @"records"]
                            ofFolder: gcsFolder]);
    }

  return rc;
}

- (BOOL) restoreUserFolderFromUserRecord: (NSDictionary *) userRecord
                             destructive: (BOOL) isDestructive
{
  NSDictionary *tables, *content;
  NSArray *restoreFolders;
  NSString *currentFolder, *folderPath;
  int count, max;
  BOOL rc;

  rc = YES;

  tables = [userRecord objectForKey: @"tables"];
  if (tables)
    {
      if ([restoreFolder isEqualToString: @"ALL"])
        restoreFolders = [tables allKeys];
      else
        {
          folderPath = [NSString stringWithFormat: @"/Users/%@/%@",
                                 userID, restoreFolder];
          restoreFolders = [NSArray arrayWithObject: folderPath];
        }
      max = [restoreFolders count];
      for (count = 0; count < max; count++)
        {
          currentFolder = [restoreFolders objectAtIndex: count];
          content = [tables objectForKey: currentFolder];
          if (content)
            rc &= [self restoreFolder: currentFolder
                          withContent: content
                          destructive: isDestructive];
          else
            {
              rc = NO;
              NSLog (@"no user table '%@' found", currentFolder);
            }
        }
    }
  else
    {
      rc = NO;
      NSLog (@"no table information found in backup file");
    }

  return rc;
}

- (BOOL) listRestorableFolders: (NSDictionary *) userRecord
{
  BOOL rc;
  NSDictionary *tables, *currentFolder;
  NSEnumerator *tableKeys;
  NSString *key, *folderKey;
  int folderPrefixLen;

  tables = [userRecord objectForKey: @"tables"];
  if (tables)
    {
      NSLog (@"Restorable folders:");
      folderPrefixLen = 8 + [userID length]; // tables keys start with /Users/<userID>
      tableKeys = [[[tables allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] objectEnumerator];
      while ((key = [tableKeys nextObject]))
        {
          currentFolder = [tables objectForKey: key];
          folderKey = [key substringFromIndex: folderPrefixLen];
          NSLog (@"  %@ (%@)",
                 folderKey, [currentFolder objectForKey: @"displayname"]);
        }
      rc = YES;
    }
  else
    {
      rc = NO;
      NSLog (@"no table information found in backup file");
    }

  return rc;
}

//
// We regenerate the Sieve script
//
- (BOOL) _updateSieveScripsForLogin: (NSString *) theLogin
{
  /* credentials file handling */
  NSException *error;
  NSString *credsFilename, *authname=nil, *authpwd=nil;
  SOGoCredentialsFile *cf;
  SOGoUser *user;
  SOGoUserFolder *home;
  SOGoMailAccounts *folder;
  SOGoMailAccount *account;
  WOContext *localContext;
  Class SOGoMailAccounts_class;

  credsFilename = [[NSUserDefaults standardUserDefaults] stringForKey: @"c"];
  if (credsFilename)
    {
      cf = [SOGoCredentialsFile credentialsFromFile: credsFilename];
      authname = [cf username];
      authpwd = [cf password];
    }

  if (authname == nil || authpwd == nil)
    {
      NSLog(@"To update Sieve scripts, you must provide the \"-c credentialFile\" parameter");
      return NO;
    }

  /* update sieve script */
  [[SOGoProductLoader productLoader] loadProducts: [NSArray arrayWithObject: @"Mailer.SOGo"]];
  SOGoMailAccounts_class = NSClassFromString(@"SOGoMailAccounts");

  user = [SOGoUser userWithLogin: theLogin];
  localContext = [WOContext context];
  [localContext setActiveUser: user];

  home = [user homeFolderInContext: localContext];
  folder = [SOGoMailAccounts_class objectWithName: @"Mail" inContainer: home];
  account = [folder lookupName: @"0" inContext: localContext acquire: NO];
  [account setContext: localContext];

  error = [account updateFiltersWithUsername: authname
                                 andPassword: authpwd
                             forceActivation: NO];
  if (error)
    return NO;

  return YES;
}

- (BOOL) restoreUserPreferencesFromUserRecord: (NSDictionary *) userRecord
{
  SOGoUser *sogoUser;
  SOGoUserProfile *up;
  NSArray *preferences;
  BOOL rc;

  preferences = [userRecord objectForKey: @"preferences"];
  if (preferences)
    {
      rc = YES;
      sogoUser = [SOGoUser userWithLogin: userID roles: nil];

      up = [[sogoUser userDefaults] source];
      [up setValues: [preferences objectAtIndex: 0]];

      if ([[NSUserDefaults standardUserDefaults] stringForKey: @"c"])
	{
	  if ([self _updateSieveScripsForLogin: userID])
	    [up synchronize];
	}
      else
	[up synchronize];

      up = [[sogoUser userSettings] source];
      [up setValues: [preferences objectAtIndex: 1]];
      [up synchronize];
    }
  else
    {
      rc = NO;
      NSLog (@"no preferences found (abort)");
    }

  return rc;
}

- (BOOL) proceed
{
  NSDictionary *userRecord;
  NSString *importPath;
  BOOL rc;

  importPath = [directory stringByAppendingPathComponent: filename];
  userRecord = [NSDictionary dictionaryWithContentsOfFile: importPath];
  if (userRecord)
    {
      if (restoreMode == SOGoToolRestoreFolderMode)
        rc = [self restoreUserFolderFromUserRecord: userRecord
                                       destructive: NO];
      else if (restoreMode == SOGoToolRestoreFolderDestructiveMode)
        rc = [self restoreUserFolderFromUserRecord: userRecord
                                       destructive: YES];
      else if (restoreMode == SOGoToolRestoreListFoldersMode)
        rc = [self listRestorableFolders: userRecord];
      else
        rc = [self restoreUserPreferencesFromUserRecord: userRecord];
    }
  else
    {
      rc = NO;
      NSLog(@"user backup (%@) file could not be loaded", importPath);
    }

  return rc;
}

- (BOOL) run
{
  [[SOGoProductLoader productLoader]
    loadProducts: [NSArray arrayWithObjects: @"Contacts.SOGo",
                           @"Appointments.SOGo",
                           nil]];
  [iCalEntityObject initializeSOGoExtensions];

  return ([self parseArguments] && [self proceed]);
}

@end
