/*
 *
 * Copyright (c) 2006 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include "qpid/acl/AclReader.h"

#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
#include "qpid/log/Statement.h"
#include "qpid/Exception.h"

#include <iomanip> // degug
#include <iostream> // debug

#define ACL_FORMAT_ERR_LOG_PREFIX "ACL format error: " << fileName << ":" << lineNumber << ": "

namespace qpid {
namespace acl {

AclReader::aclRule::aclRule(const AclResult r, const std::string n, const groupMap& groups) : res(r), actionAll(true), objStatus(NONE) {
    processName(n, groups);
}
AclReader::aclRule::aclRule(const AclResult r, const std::string n, const groupMap& groups, const Action a) : res(r), actionAll(false), action(a), objStatus(NONE) {
    processName(n, groups);
}

void AclReader::aclRule::setObjectType(const ObjectType o) {
    objStatus = VALUE;
    object = o;
}

void AclReader::aclRule::setObjectTypeAll() {
    objStatus = ALL;
}

bool AclReader::aclRule::addProperty(const Property p, const std::string v) {
    return props.insert(propNvPair(p, v)).second;
}

bool AclReader::aclRule::validate(const AclHelper::objectMapPtr& /*validationMap*/) {
    // TODO - invalid rules won't ever be called in real life...
    return true;
}

// Debug aid
std::string AclReader::aclRule::toString() {
    std::ostringstream oss;
    oss << AclHelper::getAclResultStr(res) << " [";
    for (nsCitr itr = names.begin(); itr != names.end(); itr++) {
        if (itr != names.begin()) oss << ", ";
        oss << *itr;
    }
    oss << "]";
    if (actionAll) {
        oss << " *";
    } else {
        oss << " " << AclHelper::getActionStr(action);
    }
    if (objStatus == ALL) {
        oss << " *";
    } else if (objStatus == VALUE) {
        oss << " " << AclHelper::getObjectTypeStr(object);
    }
    for (pmCitr i=props.begin(); i!=props.end(); i++) {
        oss << " " << AclHelper::getPropertyStr(i->first) << "=" << i->second;
    }
    return oss.str();
}

void AclReader::loadDecisionData( boost::shared_ptr<AclData> d )
{
    d->clear();
    QPID_LOG(debug, "ACL Load Rules");
    int cnt = rules.size();
	bool foundmode = false;
    for (rlCitr i=rules.end()-1; cnt; i--,cnt--) {
        QPID_LOG(debug, "ACL Processing " << std::setfill(' ') << std::setw(2) << cnt << " " << (*i)->toString());
		
		if (!foundmode && (*i)->actionAll && (*i)->names.size()==1 && (*((*i)->names.begin())).compare("*")==0 ){
  	       d->decisionMode = (*i)->res;
           QPID_LOG(debug, "ACL FoundMode " << AclHelper::getAclResultStr(d->decisionMode));
		   foundmode=true;
		}else{
		    AclData::rule rule((*i)->props);
		    bool addrule= true;
		
		    switch ((*i)->res)
		    {
		    case qpid::acl::ALLOWLOG:
		        rule.log = true;
                if (d->decisionMode == qpid::acl::ALLOW || d->decisionMode == qpid::acl::ALLOWLOG) 
				    rule.logOnly = true;
				break;
	    	case qpid::acl::ALLOW:
                if (d->decisionMode == qpid::acl::ALLOW || d->decisionMode == qpid::acl::ALLOWLOG)
				    addrule = false; 
                break;
		    case qpid::acl::DENYLOG:
			    rule.log = true;
                if (d->decisionMode == qpid::acl::DENY || d->decisionMode == qpid::acl::DENYLOG) 
				    rule.logOnly = true;
                break;
		    case qpid::acl::DENY:
                if (d->decisionMode == qpid::acl::DENY || d->decisionMode == qpid::acl::DENYLOG)
				    addrule = false; 
			    break;
		    default:
		        throw Exception("Invalid ACL Result loading rules.");
		    }
		    
			
            // Action -> Object -> map<user -> set<Rule> >
            if (addrule){
				for (int acnt= ((*i)->actionAll?0:(*i)->action); 
        				 acnt< acl::ACTIONSIZE; (*i)->actionAll?acnt++:acnt=acl::ACTIONSIZE )  {

            		if (acnt == acl::ACT_PUBLISH) d->transferAcl = true; // we have transfer ACL
					
					QPID_LOG(debug, "ACL Adding action:" << AclHelper::getActionStr((Action)acnt) );

	        		//find the Action, create if not exist
					if (d->actionList[acnt]==NULL) {
			    		d->actionList[acnt] = new AclData::aclAction[qpid::acl::OBJECTSIZE];
                		for (int j=0;j<qpid::acl::OBJECTSIZE; j++)
                    		d->actionList[acnt][j] = NULL;
            		}

					// optimize this loop to limit to valid options only!!
					for (int ocnt= ((*i)->objStatus!=aclRule::VALUE ?0:(*i)->object); 
        					 ocnt< acl::OBJECTSIZE; 
							 (*i)->objStatus!=aclRule::VALUE?ocnt++:ocnt=acl::OBJECTSIZE )  {

            				QPID_LOG(debug, "ACL Adding object:" << AclHelper::getObjectTypeStr((ObjectType)ocnt) );

							//find the Object, create if not exist
							if (d->actionList[acnt][ocnt] == NULL) 
	            				d->actionList[acnt][ocnt] = new AclData::actionObject;

            				// add users and Rule to object set
							bool allNames=false;
							// check to see if names.begin is '*'
							if ( (*(*i)->names.begin()).compare("*")==0 ) allNames = true;

							for (nsCitr itr = (allNames?names.begin():(*i)->names.begin());
						        	  itr != (allNames?names.end():(*i)->names.end()); itr++) {
							   AclData::actObjItr itrRule = d->actionList[acnt][ocnt]->find(*itr);
							   if (itrRule == d->actionList[acnt][ocnt]->end()) {
                				   QPID_LOG(debug, "ACL Adding rule & user:" << *itr);
			    				   AclData::ruleSet rSet;
			    				   rSet.push_back(rule);
                				   d->actionList[acnt][ocnt]->insert(make_pair( std::string(*itr) , rSet) );
            				   }else{
							   
							   // TODO add code to check for dead rules
							   // allow peter create queue name=tmp <-- dead rule!!
							   // allow peter create queue
							   
			   					   itrRule->second.push_back(rule);
                				   QPID_LOG(debug, "ACL Adding rule to user:" << *itr);
							   }
            				}

	        		}

        	   }
		   }else{
            	QPID_LOG(debug, "ACL Skipping based on Mode:" << AclHelper::getAclResultStr(d->decisionMode) );
		   }
      }		
		
    }


}




void AclReader::aclRule::processName(const std::string& name, const groupMap& groups) {
    if (name.compare("all") == 0) {
        names.insert("*");
    } else {
        gmCitr itr = groups.find(name);
        if (itr == groups.end()) {
            names.insert(name);
        } else {
            names.insert(itr->second->begin(), itr->second->end());
        }
    }
}

AclReader::AclReader() : lineNumber(0), contFlag(false), validationMap(new AclHelper::objectMap) {
    AclHelper::loadValidationMap(validationMap);
	names.insert("*");
}

AclReader::~AclReader() {}

std::string AclReader::getError() {
	return errorStream.str();
}

int AclReader::read(const std::string& fn, boost::shared_ptr<AclData> d) {
    fileName = fn;
    lineNumber = 0;
    char buff[1024];
    std::ifstream ifs(fn.c_str(), std::ios_base::in);
    if (!ifs.good()) {
        errorStream << "Unable to open ACL file \"" << fn << "\": eof=" << (ifs.eof()?"T":"F") << "; fail=" << (ifs.fail()?"T":"F") << "; bad=" << (ifs.bad()?"T":"F");
        return -1;
    }
    try {
        bool err = false;
        while (ifs.good()) {
            ifs.getline(buff, 1024);
            lineNumber++;
            if (std::strlen(buff) > 0 && buff[0] != '#') // Ignore blank lines and comments
                err |= !processLine(buff);
        }
        if (!ifs.eof())
        {
            errorStream << "Unable to read ACL file \"" << fn << "\": eof=" << (ifs.eof()?"T":"F") << "; fail=" << (ifs.fail()?"T":"F") << "; bad=" << (ifs.bad()?"T":"F");
            ifs.close();
            return -2;
        }
        ifs.close();
        if (err) return -3;
        QPID_LOG(notice, "Read ACL file \"" <<  fn << "\"");
    } catch (const std::exception& e) {
        errorStream << "Unable to read ACL file \"" << fn << "\": " << e.what();
        ifs.close();
        return -4;
    } catch (...) {
        errorStream << "Unable to read ACL file \"" << fn << "\": Unknown exception";
        ifs.close();
        return -5;
    }
    printNames();
    printRules();
	loadDecisionData(d);

    return 0;
}

bool AclReader::processLine(char* line) {
    bool ret = false;
    std::vector<std::string> toks;

    // Check for continuation
    char* contCharPtr = std::strrchr(line, '\\');
    bool cont = contCharPtr != 0;
    if (cont) *contCharPtr = 0;

    int numToks = tokenize(line, toks);
    if (numToks && (toks[0].compare("group") == 0 || contFlag)) {
        ret = processGroupLine(toks, cont);
    } else if (numToks && toks[0].compare("acl") == 0) {
        ret = processAclLine(toks);
    } else {
        // Check for whitespace only line, ignore these
        bool ws = true;
        for (unsigned i=0; i<std::strlen(line) && ws; i++) {
            if (!std::isspace(line[i])) ws = false;
        }
        if (ws) {
            ret = true;
        } else {
            errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Non-continuation line must start with \"group\" or \"acl\".";
            ret = false;
        }
    }
    contFlag = cont;
    return ret;
}

int  AclReader::tokenize(char* line, std::vector<std::string>& toks) {
    const char* tokChars = " \t\n\f\v\r";
    int cnt = 0;
    char* cp = std::strtok(line, tokChars);
    while (cp != 0) {
        toks.push_back(std::string(cp));
        cnt++;
        cp = std::strtok(0, tokChars);
    }
    return cnt;
}

// Return true if the line is successfully processed without errors
// If cont is true, then groupName must be set to the continuation group name
bool AclReader::processGroupLine(tokList& toks, const bool cont) {
    const unsigned toksSize = toks.size();
    if (contFlag) {
        gmCitr citr = groups.find(groupName);
        for (unsigned i = 0; i < toksSize; i++) {
            if (!checkName(toks[i])) {
                errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Name \"" << toks[i] << "\" contains illegal characters.";
                return false;
            }
            addName(toks[i], citr->second);
        }
    } else {
        if (toksSize < (cont ? 2 : 3)) {
            errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Insufficient tokens for group definition.";
            return false;
        }
        if (!checkName(toks[1])) {
            errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Group name \"" << toks[1] << "\" contains illegal characters.";
            return false;
        }
        gmCitr citr = addGroup(toks[1]);
        if (citr == groups.end()) return false;
        for (unsigned i = 2; i < toksSize; i++) {
            if (!checkName(toks[i])) {
                errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Name \"" << toks[i] << "\" contains illegal characters.";
                return false;
            }
            addName(toks[i], citr->second);
        }
    }
    return true;
}

// Return true if sucessfully added group
AclReader::gmCitr AclReader::addGroup(const std::string& newGroupName) {
    gmCitr citr = groups.find(newGroupName);
    if (citr != groups.end()) {
        errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Duplicate group name \"" << newGroupName << "\".";
        return groups.end();
    }
    groupPair p(newGroupName, nameSetPtr(new nameSet));
    gmRes res = groups.insert(p);
    assert(res.second);
    groupName = newGroupName;
    return res.first;
}

void AclReader::addName(const std::string& name, nameSetPtr groupNameSet) {
    gmCitr citr = groups.find(name);
    if (citr != groups.end()) {
        // This is a previously defined group: add all the names in that group to this group
        groupNameSet->insert(citr->second->begin(), citr->second->end());
    } else {
        // Not a known group name
        groupNameSet->insert(name);
        addName(name);
    }
}

void AclReader::addName(const std::string& name) {
    names.insert(name);
}

// Debug aid
void AclReader::printNames() const {
    QPID_LOG(debug, "Group list: " << groups.size() << " groups found:" );
    std::string tmp;
	for (gmCitr i=groups.begin(); i!= groups.end(); i++) {
        tmp += "  \"";
		tmp += i->first;
		tmp +=  "\":";
        for (nsCitr j=i->second->begin(); j!=i->second->end(); j++) {
            tmp += " ";
			tmp += *j;
        }
        QPID_LOG(debug, tmp);
		tmp.clear();
    }
	QPID_LOG(debug, "Name list: " << names.size() << " names found:" );
	tmp.clear();
    for (nsCitr k=names.begin(); k!=names.end(); k++) {
        tmp += " ";
		tmp += *k;
    }
    QPID_LOG(debug, tmp);
}

bool AclReader::processAclLine(tokList& toks) {
    const unsigned toksSize = toks.size();
    if (toksSize < 4) {
        errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Insufficient tokens for acl definition.";
        return false;
    }

    AclResult res;
    try {
        res = AclHelper::getAclResult(toks[1]);
    } catch (...) {
        errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Unknown ACL permission \"" << toks[1] << "\".";
        return false;
    }

    bool actionAllFlag = toks[3].compare("all") == 0;
    bool userAllFlag   = toks[2].compare("all") == 0;
    Action action;
    if (actionAllFlag) {

        if (userAllFlag && toksSize > 4) {
            errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Tokens found after action \"all\".";
            return false;
        }
        action = ACT_CONSUME; // dummy; compiler must initialize action for this code path
    } else {
        try {
            action = AclHelper::getAction(toks[3]);
        } catch (...) {
            errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Unknown action \"" << toks[3] << "\".";
            return false;
        }
    }

    // Create rule obj; then add object (if any) and properties (if any)
    aclRulePtr rule;
    if (actionAllFlag) {
        rule.reset(new aclRule(res, toks[2], groups));
    } else {
        rule.reset(new aclRule(res, toks[2], groups, action));
    }
    
    if (toksSize >= 5) { // object name-value pair
        if (toks[4].compare("all") == 0) {
            rule->setObjectTypeAll();
        } else {
            try {
                rule->setObjectType(AclHelper::getObjectType(toks[4]));
            } catch (...) {
                errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Unknown object \"" << toks[4] << "\".";
                return false;
            }
        }
    }

    if (toksSize >= 6) { // property name-value pair(s)
        for (unsigned i=5; i<toksSize; i++) {
            nvPair propNvp = splitNameValuePair(toks[i]);
            if (propNvp.second.size() == 0) {
                errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Badly formed property name-value pair \"" << propNvp.first << "\". (Must be name=value)";
                return false;
            }
            Property prop;
            try {
                prop = AclHelper::getProperty(propNvp.first);
            } catch (...) {
                errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Unknown property \"" << propNvp.first << "\".";
                return false;
            }
            rule->addProperty(prop, propNvp.second);
        }
    }
    // Check if name (toks[2]) is group; if not, add as name of individual
    if (toks[2].compare("all") != 0) {
        if (groups.find(toks[2]) == groups.end()) {
            addName(toks[2]);
        }
    }

    // If rule validates, add to rule list
    if (!rule->validate(validationMap)) {
        errorStream << ACL_FORMAT_ERR_LOG_PREFIX << "Invalid object/action/property combination.";
        return false;
    }
    rules.push_back(rule);

    return true;
}

// Debug aid
void AclReader::printRules() const {
    QPID_LOG(debug, "Rule list: " << rules.size() << " ACL rules found:");
    int cnt = 0;
    for (rlCitr i=rules.begin(); i<rules.end(); i++,cnt++) {
        QPID_LOG(debug, "  " << std::setfill(' ') << std::setw(2) << cnt << " " << (*i)->toString());
    }
}

// Static function
// Return true if the name is well-formed (ie contains legal characters)
bool AclReader::checkName(const std::string& name) {
    for (unsigned i=0; i<name.size(); i++) {
        const char ch = name.at(i);
        if (!std::isalnum(ch) && ch != '-' && ch != '_' && ch != '@') return false;
    }
    return true;
}

// Static function
// Split name-value pair around '=' char of the form "name=value"
AclReader::nvPair AclReader::splitNameValuePair(const std::string& nvpString) {
    std::size_t pos = nvpString.find("=");
    if (pos == std::string::npos || pos == nvpString.size() - 1) {
        return nvPair(nvpString, "");
    }
    return nvPair(nvpString.substr(0, pos), nvpString.substr(pos+1));
}

}} // namespace qpid::acl
