/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2001-02 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.venice.core.impl;

import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import com.silverwrist.util.International;
import com.silverwrist.util.OptionSet;
import com.silverwrist.venice.core.*;
import com.silverwrist.venice.core.internals.*;
import com.silverwrist.venice.db.*;
import com.silverwrist.venice.except.*;
import com.silverwrist.venice.security.*;

class AdminUserContextImpl implements AdminUserContext
{
  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Category logger = Category.getInstance(AdminUserContextImpl.class);

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private EnvUser env;                     // the local environment
  private int uid;                         // the user ID of this user
  private int contactid;                   // ID of associated contact information
  private int level;                       // base security level for this user
  private boolean email_verified;          // has email address been verified?
  private boolean lockout;                 // is this account locked out?
  private String username;                 // the user name we're using
  private java.util.Date created;          // when was this user created? (GMT)
  private java.util.Date last_access;      // when did we last log in? (GMT)
  private String description;              // personal description
  private Locale my_locale;                // my default locale (cached)
  private TimeZone my_tz;                  // my default timezone (cached)
  private OptionSet flags = null;          // option flags

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  protected AdminUserContextImpl(Connection conn, EnvUser env, ResultSet rs) throws SQLException
  {
    this.env = env;
    this.uid = rs.getInt("uid");
    this.contactid = rs.getInt("contactid");
    this.level = rs.getInt("base_lvl");
    this.email_verified = rs.getBoolean("verify_email");
    this.lockout = rs.getBoolean("lockout");
    this.username = rs.getString("username");
    this.created = SQLUtil.getFullDateTime(rs,"created");
    this.last_access = SQLUtil.getFullDateTime(rs,"lastaccess");
    this.description = rs.getString("description");
    this.my_locale = International.get().createLocale(rs.getString("localeid"));
    this.my_tz = TimeZone.getTimeZone(rs.getString("tzid"));

    Statement stmt = null;

    try
    { // get user properties
      stmt = conn.createStatement();
      ResultSet rs2 = stmt.executeQuery("SELECT ndx, data FROM propuser WHERE uid = " + this.uid + ";");
      while (rs2.next())
      { // load the properties...
	switch (rs2.getInt(1))
	{ // based on the property, do what is needed
	  case UserContextImpl.PROP_FLAGS:
	    flags = new OptionSet(rs2.getString(2));
	    break;

	  default:
	    break;

	} // end switch

      } // end while

    } // end try
    finally
    { // close statement to conserve resources
      SQLUtil.shutdown(stmt);

    } // end finally

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private final void updateProperties(BitSet delta) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    try
    { // get a connection and create a statement
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer();

      if (delta.get(UserContextImpl.PROP_FLAGS))
      { // store the flags
	sql.append("UPDATE propuser SET data = '").append(flags.asString()).append("' WHERE uid = ");
	sql.append(uid).append(" AND ndx = ").append(UserContextImpl.PROP_FLAGS).append(';');
	stmt.executeUpdate(sql.toString());
	sql.setLength(0);

      } // end if

    } // end try
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error storing user properties: " + e.getMessage(),e);
      throw new DataException("unable to save user properties: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end updateProperties

  private final void updateProperties() throws DataException
  {
    updateProperties(UserContextImpl.ALL_PROPS);

  } // end updateProperties

  private final AdminUserProperties createProperties()
  {
    AdminUserProperties rc = new AdminUserProperties();
    rc.setDisplayPostPictures(flags.get(UserContextImpl.BF_POSTPICTURES));
    rc.setDisallowPhoto(flags.get(UserContextImpl.BF_ADM_NOPHOTO));
    rc.setMassMailOptOut(flags.get(UserContextImpl.BF_MASSMAIL_OPTOUT));
    return rc;

  } // end createProperties

  private final BitSet storeProperties(AdminUserProperties props)
  {
    BitSet rc = new BitSet();

    if (flags.assign(UserContextImpl.BF_POSTPICTURES,props.getDisplayPostPictures()))
      rc.set(UserContextImpl.PROP_FLAGS);
    if (flags.assign(UserContextImpl.BF_ADM_NOPHOTO,props.getDisallowPhoto()))
      rc.set(UserContextImpl.PROP_FLAGS);
    if (flags.assign(UserContextImpl.BF_MASSMAIL_OPTOUT,props.getMassMailOptOut()))
      rc.set(UserContextImpl.PROP_FLAGS);

    return rc;

  } // end storeProperties

  /*--------------------------------------------------------------------------------
   * Implementations from interface AdminUserContext
   *--------------------------------------------------------------------------------
   */

  public int getUID()
  {
    return uid;

  } // end getUID

  public String getUserName()
  {
    return username;

  } // end getUserName

  public int getContactID()
  {
    return contactid;

  } // end getContactID

  public String getDescription()
  {
    return description;

  } // end getDescription

  public void setDescription(String new_descr) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    if (new_descr.equals(description))
      return;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE users SET description = '");
      sql.append(SQLUtil.encodeString(new_descr)).append("' WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

      description = new_descr;  // change stored information
      ar = env.newAudit(AuditRecord.ADMIN_ACCOUNT_CHANGE,"uid=" + uid,"field=description");
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing description: " + e.getMessage(),e);
      throw new DataException("Unable to set user description: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setDescription

  public int getBaseLevel()
  {
    return level;

  } // end getBaseLevel

  public void setBaseLevel(int new_level) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    if (level==new_level)
      return;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE users SET base_lvl = ");
      sql.append(new_level).append(" WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

      level = new_level;
      ar = env.newAudit(AuditRecord.ADMIN_SET_SECURITY,"uid=" + uid,"level=" + new_level);
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing base level: " + e.getMessage(),e);
      throw new DataException("Unable to set user base level: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setBaseLevel

  public Role getBaseRole()
  {
    return env.getRoleForLevel(level);

  } // end getBaseRole

  public void setBaseRole(Role new_role) throws DataException
  {
    setBaseLevel(new_role.getLevel());

  } // end setBaseRole

  public boolean isEmailVerified()
  {
    return email_verified;

  } // end isEmailVerified

  public void setEmailVerified(boolean flag) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    if (flag==email_verified)
      return;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE users SET verify_email = ");
      sql.append(flag ? '1' : '0').append(" WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

      email_verified = flag;
      ar = env.newAudit(AuditRecord.ADMIN_ACCOUNT_CHANGE,"uid=" + uid,"field=verify_email");
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing verify flag: " + e.getMessage(),e);
      throw new DataException("Unable to set user verify flag: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setEmailVerified

  public boolean isLockedOut()
  {
    return lockout;

  } // end isLockedOut

  public void setLockedOut(boolean flag) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    if (flag==lockout)
      return;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();
      StringBuffer sql = new StringBuffer("UPDATE users SET lockout = ");
      sql.append(flag ? '1' : '0').append(" WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

      lockout = flag;
      ar = env.newAudit(AuditRecord.ADMIN_LOCK_OUT,"uid=" + uid,flag ? "locked" : "unlocked");
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing lockout flag: " + e.getMessage(),e);
      throw new DataException("Unable to set user lockout flag: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setLockedOut

  public ContactInfo getContactInfo() throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("getContactInfo() for UID " + uid);

    ContactInfoImpl rc;
    if (contactid>=0)
      rc = new ContactInfoImpl(env,contactid,true);
    else
      rc = new ContactInfoImpl(uid,true);
    return rc;

  } // end getContactInfo

  public void putContactInfo(ContactInfo ci) throws DataException
  {
    if (logger.isDebugEnabled())
      logger.debug("putContactInfo() for UID " + uid);

    if ((ci.getOwnerUID()!=uid) || (ci.getOwnerCommunityID()>=0))
    { // the contact information is not owned correctly
      logger.error("ContactInfo ownership wrong (it's " + ci.getOwnerUID() + ", " + ci.getOwnerCommunityID()
		   + "), should be (" + uid + ", -1)");
      throw new DataException("invalid contact information record");

    } // end if

    Connection conn = null;  // database connection
    AuditRecord ar = null;   // audit record
      
    try
    { // get a database connection
      conn = env.getConnection();
      Stashable obj = (Stashable)ci;

      // save the contact information
      obj.stash(conn);

      if (contactid<0)
      { // contact being established for the first time
	contactid = ci.getContactID();
	if (logger.isDebugEnabled())
	  logger.debug("...established initial contact (" + contactid + ") for user");

      } // end if

      ar = env.newAudit(AuditRecord.ADMIN_USER_CONTACT_INFO,"uid=" + uid,"contactid=" + contactid);

    } // end try
    catch (ClassCastException cce)
    { // we need to be able to coerce the ContactInfo to a Stashable
      logger.error("ContactInfo needs to be a Stashable for this to work");
      throw new DataException("improper contact information record");

    } // end catch
    catch (SQLException e)
    { // database error - this is a DataException
      logger.error("DB error updating contact info: " + e.getMessage(),e);
      throw new DataException("unable to access user contact data: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end if

  } // end putContactInfo

  public void setPassword(String password, String reminder) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();
      PasswordHash phash = new PasswordHash(password);
      StringBuffer sql = new StringBuffer("UPDATE users SET passhash = '");
      sql.append(phash.toString()).append("', passreminder = '").append(SQLUtil.encodeString(reminder));
      sql.append("', access_tries = 0 WHERE uid = ").append(uid).append(';');
      stmt.executeUpdate(sql.toString());

      // record an audit record for this user
      ar = env.newAudit(AuditRecord.ADMIN_PASSWORD_CHANGE,"uid=" + uid);
      
    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error changing password: " + e.getMessage(),e);
      throw new DataException("Unable to set user password: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setPassword

  public Locale getLocale()
  {
    return my_locale;

  } // end getLocale

  public void setLocale(Locale locale) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the update statement
      StringBuffer sql = new StringBuffer("UPDATE userprefs SET localeid = '");
      sql.append(SQLUtil.encodeString(locale.toString())).append("' WHERE uid = ").append(uid).append(';');

      // execute the statement
      stmt.executeUpdate(sql.toString());

      // replace the locale here
      my_locale = locale;
      ar = env.newAudit(AuditRecord.ADMIN_ACCOUNT_CHANGE,"uid=" + uid,"field=localeid");

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting user locale: " + e.getMessage(),e);
      throw new DataException("unable to set user locale: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setLocale

  public TimeZone getTimeZone()
  {
    return my_tz;

  } // end getTimeZone

  public void setTimeZone(TimeZone timezone) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;
    AuditRecord ar = null;

    try
    { // retrieve a connection from the data pool
      conn = env.getConnection();
      stmt = conn.createStatement();

      // create the update statement
      StringBuffer sql = new StringBuffer("UPDATE userprefs SET tzid = '");
      sql.append(SQLUtil.encodeString(timezone.getID())).append("' WHERE uid = ").append(uid).append(';');

      // execute the statement
      stmt.executeUpdate(sql.toString());

      // replace the locale here
      my_tz = timezone;
      ar = env.newAudit(AuditRecord.ADMIN_ACCOUNT_CHANGE,"uid=" + uid,"field=tzid");

    } // end try
    catch (SQLException e)
    { // turn SQLException into data exception
      logger.error("DB error setting user timezone: " + e.getMessage(),e);
      throw new DataException("unable to set user timezone: " + e.getMessage(),e);

    } // end catch
    finally
    { // make sure the connection is released before we go
      SQLUtil.shutdown(stmt);
      AuditRecord.store(conn,ar);
      env.releaseConnection(conn);

    } // end finally

  } // end setTimeZone

  public java.util.Date getCreationDate()
  {
    return created;

  } // end getCreationDate

  public java.util.Date getLastAccessDate()
  {
    return last_access;

  } // end getLastAccessDate

  public AdminUserProperties getProperties() throws DataException
  {
    return createProperties();

  } // end getProperties

  public void setProperties(AdminUserProperties props) throws DataException
  {
    AdminUserProperties orig = createProperties();
    BitSet delta = storeProperties(props);
    if (delta.length()==0)
      return;

    boolean succeeded = false;
    try
    { // update the properties in the database
      updateProperties(delta);
      succeeded = true;

    } // end try
    finally
    { // make sure the connection is released before we go
      if (!succeeded)
	storeProperties(orig);

    } // end finally

  } // end setProperties

  /*--------------------------------------------------------------------------------
   * Package-level static operations
   *--------------------------------------------------------------------------------
   */

  static AdminUserContext getAdminUserContext(EnvUser env, int uid) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT * FROM users INNER JOIN userprefs "
				       + "ON users.uid = userprefs.uid WHERE users.uid = " + uid + ";");
      if (!(rs.next()))
	throw new DataException("The user with UID #" + uid + " was not found.");
      if (rs.getBoolean("is_anon"))
	throw new DataException("Cannot modify the defaults for the anonymous user.");

      return new AdminUserContextImpl(conn,env,rs);

    } // end try
    catch (SQLException e)
    { // we encountered an error!
      logger.error("DB exception in getAdminUserContext: " + e.getMessage(),e);
      throw new DataException("Unable to load context for user: " + e.getMessage());

    } // end catch
    finally
    { // release the connection where necessary
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end getAdminUserContext

  static AdminUserContext getAdminUserContext(EnvUser env, String username) throws DataException
  {
    Connection conn = null;
    Statement stmt = null;

    try
    { // get a database connection
      conn = env.getConnection();
      stmt = conn.createStatement();
      ResultSet rs = stmt.executeQuery("SELECT * FROM users INNER JOIN userprefs "
				       + "ON users.uid = userprefs.uid WHERE users.username = '"
				       + SQLUtil.encodeString(username) + "';");
      if (!(rs.next()))
	throw new DataException("The user '" + username + "' was not found.");
      if (rs.getBoolean("is_anon"))
	throw new DataException("Cannot modify the defaults for the anonymous user.");

      return new AdminUserContextImpl(conn,env,rs);

    } // end try
    catch (SQLException e)
    { // we encountered an error!
      logger.error("DB exception in getAdminUserContext: " + e.getMessage(),e);
      throw new DataException("Unable to load context for user: " + e.getMessage());

    } // end catch
    finally
    { // release the connection where necessary
      SQLUtil.shutdown(stmt);
      env.releaseConnection(conn);

    } // end finally

  } // end getAdminUserContext

} // end class AdminUserContextImpl
