package io.netfoundry.identity.service.controller.resource;

import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.PagedModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.netfoundry.auth.clientv2.AuthorizationContext;
import io.netfoundry.auth.clientv2.annotation.AuthAction;
import io.netfoundry.auth.clientv2.annotation.AuthDelete;
import io.netfoundry.auth.clientv2.annotation.AuthRead;
import io.netfoundry.auth.clientv2.annotation.AuthUpdate;
import io.netfoundry.auth.domainv2.ancestry.ResourceIdPath;
import io.netfoundry.auth.domainv2.domain.Domain;
import io.netfoundry.common.util.entity.relation.CanonicalGetMapping;
import io.netfoundry.common.util.jdbc.sql.WhereClause;
import io.netfoundry.common.util.validation.ValidAs;
import io.netfoundry.common.util.validation.ValidAs.AcceptNull;
import io.netfoundry.identity.Application;
import io.netfoundry.identity.domain.identity.Identity;
import io.netfoundry.identity.domain.identity.IdentityService;
import io.netfoundry.identity.domain.idprovider.IdentityProvider;
import io.netfoundry.identity.domain.idprovider.IdentityProviderService;
import io.netfoundry.identity.domain.organization.Auth0ConnectionType;
import io.netfoundry.identity.domain.organization.MfaProvider;
import io.netfoundry.identity.domain.organization.Organization;
import io.netfoundry.identity.domain.organization.OrganizationService;
import io.netfoundry.identity.service.logic.authorization.AuthorizationService;
import io.netfoundry.identity.service.logic.networkgroup.NetworkGroupService;
import io.netfoundry.identity.service.logic.networkgroup.NetworkGroupService.NetworkGroupResult;
import io.netfoundry.identity.util.HalUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;

@Validated
@RestController
@Slf4j
public class OrganizationController {

  private final OrganizationService organizationService;
  private final IdentityProviderService identityProviderService;
  private final NetworkGroupService networkGroupService;
  private final AuthorizationService authorizationService;
  private final IdentityService identityService;
  private final Domain defaultDomain;
  
  
  @Autowired
  public OrganizationController(
      OrganizationService organizationService,
      IdentityProviderService identityProviderService,
      NetworkGroupService networkGroupService,
      AuthorizationService authorizationService,
      IdentityService identityService,
      Domain defaultDomain
      ) {
    this.organizationService = organizationService;
    this.identityProviderService = identityProviderService;
    this.networkGroupService = networkGroupService;
    this.authorizationService = authorizationService;
    this.identityService = identityService;
    this.defaultDomain = defaultDomain;
  }

  
  
  @GetMapping(path = {
      "/tenants",  // deprecated
      "/organizations"
  })
  public PagedModel<EntityModel<Organization>> findOrganizations(
      @RequestParam(name="name", required=false) String name,
      @RequestParam(name="active", required=false) Boolean active,
      @RequestParam(name="mfaProviders", required=false) Set<MfaProvider> mfaProviders,
      @RequestParam(name="includeDeleted", required=false, defaultValue = "false") boolean includeDeleted,
      @AuthRead(on = Organization.class) WhereClause authConstraint,
      Pageable pageable
  ) {
    PagedModel<EntityModel<Organization>> relink = null;
    try {
      relink = relink(organizationService.findOrganizations(name, active,
          (includeDeleted) ? /* deleted */ null : includeDeleted,
          mfaProviders == null ? null : () -> mfaProviders, authConstraint, pageable));
    } catch (Exception e) {
      log.error("Error while calling findOrganizations {}", e.getMessage());
      e.printStackTrace();
    }

    return relink;
  }
  
  
  @CanonicalGetMapping(path = {
      "/tenants/{id:"+Application.UUID_REGEX+"}",  // deprecated
      "/organizations/{id:"+Application.UUID_REGEX+"}"
  })
  public ResponseEntity<Organization> getOrganizationById(
      @AuthRead(on=Organization.class) @PathVariable(name="id") UUID id
      ) {
    Organization organization = organizationService.getOrganization(id).orElse(null);
    return new ResponseEntity<>(organization, HttpStatus.OK);
  }
  
  
    // anonymous request
  @GetMapping(path = {
      "/tenants/label/{label:[-a-zA-Z0-9]{1,63}}",  // deprecated
      "/organizations/label/{label:[-a-zA-Z0-9]{1,63}}"
  })
  public ResponseEntity<Organization> getOrganizationByLabel(@PathVariable(name="label") String label) {
    Organization organization = organizationService.getOrganization(label).orElse(null);
    if (organization == null) {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    return new ResponseEntity<>(organization, HttpStatus.OK);
  }
  
  
  @Value
  public static class CreateCommand {
    @ValidAs(field = "name", on = Organization.class)
    private final String name;
    @ValidAs(field = "label", on = Organization.class)
    private final String label;
    /*
     * Only the Organization name and label are required now.  We used to require IdP at creation,
     * but it has since been broken out as its own resource with its own controller.  Support for
     * adding an IdP at Org creation time is maintained here for backwards compatibility. 
     */
    @ValidAs(on = IdentityProvider.class, acceptNull = AcceptNull.ALWAYS)
    private final String auth0ConnectionId;
    @ValidAs(field = "name", on = IdentityProvider.class, acceptNull = AcceptNull.ALWAYS)
    private final String identityProviderName;
    @ValidAs(on = IdentityProvider.class, acceptNull = AcceptNull.ALWAYS)
    private final Auth0ConnectionType auth0ConnectionType;
    
    private boolean hasValidIdPInfo() {
      boolean hasIdpInfo = auth0ConnectionId != null;
      if ((hasIdpInfo ^ identityProviderName != null) ||
          (hasIdpInfo ^ auth0ConnectionType != null)) {
        throw new IllegalArgumentException("Invalid Identity Provider settings.  Either all ("
            + "auth0ConnectionId, identityProviderName, auth0ConnectionType) or none required.");
      }
      return hasIdpInfo;
    }
  }
  
  @PostMapping(path = {
      "/tenants",  // deprecated
      "/organizations"
  })
  @Transactional
  public ResponseEntity<Organization> createOrganization(
      HttpServletRequest request,
      @Valid @RequestBody CreateCommand command
      ) {
    // create organization is not typically granted...
    AuthorizationContext context = AuthorizationContext.getContext();
    if (context == null) throw new IllegalStateException("Authenticated identity required.");
    boolean isAuthorized = context.isAuthorized(
        "io.netfoundry.identity", "organization", "create", 
        ResourceIdPath.empty(defaultDomain.getId()));
    if (!isAuthorized) throw new IllegalStateException("Not Authorized.");
    
    Identity identity = identityService.getIdentity(context.getIdentityId()).orElse(null);
    if (identity == null) throw new IllegalStateException("Valid identity required.");
    
    Organization organization = organizationService.createOrganization(
        command.getName(), 
        command.getLabel());
    
    if (command.hasValidIdPInfo()) {
      identityProviderService.create(
          organization.getId(), 
          command.getAuth0ConnectionId(), 
          command.getName(), 
          command.getAuth0ConnectionType());
      
      // re-get it so that the returned instance contains the identityProvider
      organization = organizationService.getOrganization(organization.getId())
          .orElseThrow(IllegalStateException::new);
    }
    
    /*
     * Note, we do not grant permissions to the API client that made this create Organization request.
     * 
     * At the time of this writing, CMS is the only identity with permission to call this API endpoint,
     * and it is expected that any service identity or future identity whom we grant access to create 
     * this "domain root resource" ... will also have permission to grant standard roles related to 
     * the resulting Organization.
     * 
     * That being said, we will create the standard roles (and stop short of granting them to anyone.)
     */
    authorizationService.getOrCreateOrganizationAdmin(organization);
    
    
    return new ResponseEntity<>(organization, HttpStatus.CREATED);
  }
  
  
  @PutMapping(path = {
      "/tenants/{id}/{action:activate|deactivate}",  // deprecated
      "/organizations/{id}/{action:activate|deactivate}"
  })
  public ResponseEntity<Organization> activateOrDeactivateOrganization(
      HttpServletRequest request,
      @AuthAction(action="update-active",on=Organization.class) @PathVariable("id") UUID id,
      @PathVariable("action") String action
      ) {
    Organization organization = organizationService.getOrganization(id)
        .orElseThrow( () -> new IllegalArgumentException("No such Organization by that id: "+id));
    
    if ("activate".equals(action)) {
      organization = organizationService.activateOrganization(organization);
    }
    else {
      // Business rule:  An identity can not deactivate the organization they are logged into.
      AuthorizationContext authContext = AuthorizationContext.getContext();
      Identity identity = identityService.getIdentity(authContext.getIdentityId())
          .orElseThrow(() -> new IllegalStateException("No identity found for identity id from "
              + "auth context: "+authContext.getIdentityId()));
      if (id.equals(identity.getOrganizationId())) {
        // TEMPORARY - Allow self-termination of organization so that the console can terminate access to
        // the MOP when a client leaves.  At some future point we will have a service that provides
        // a single 'terminate' endpoint and which calls the internal identity, billing and edge 
        // endpoints necessary to terminate each respective domains resources.
        
//        throw new IllegalArgumentException("You can not deactivate the organization that you are logged "
//            + "into.");
      }
      organization = organizationService.deactivateOrganization(organization);
    }
    
    return new ResponseEntity<>(organization, HttpStatus.OK);
  }
  
  
  @Value
  @ValidAs(on = Organization.class, acceptNull = AcceptNull.ALWAYS)
  public static class UpdateOrganizationCommand {
    private final String name;
    private final MfaProvider mfaProvider;
  }
  
  @PutMapping(path = {
      "/tenants/{id}",  // deprecated
      "/organizations/{id}"
  })
  public ResponseEntity<Organization> updateOrganization(
      @AuthUpdate(on=Organization.class) @PathVariable("id") UUID id,
      @Valid @RequestBody UpdateOrganizationCommand command
      ) {
    Organization organization = organizationService.getOrganization(id).orElse(null);
    
    Supplier<MfaProvider> mfaProvider = null;  // don't update MFA setting if null.
    if (command.getMfaProvider() != null) mfaProvider = () -> command.getMfaProvider();
    
    return new ResponseEntity<>(
        organizationService.updateOrganization(organization, command.getName(), mfaProvider),
        HttpStatus.OK);
  }
  
  @DeleteMapping(path = {
      "/tenants/{id}",  // deprecated
      "/organizations/{id}"
  })
  public ResponseEntity<Organization> deleteOrganization(
      HttpServletRequest request,
      @AuthDelete(on=Organization.class) @PathVariable("id") UUID id
      ) {
    Organization organization = organizationService.getOrganization(id).orElse(null);
    
    AuthorizationContext authContext = AuthorizationContext.getContext();
    Identity identity = identityService.getIdentity(authContext.getIdentityId())
        .orElseThrow(() -> new IllegalStateException("No identity found for identity id from "
            + "auth context: "+authContext.getIdentityId()));
    if (id.equals(identity.getOrganizationId())) {
      throw new IllegalArgumentException("You can not delete the organization that you are logged into.");
    }
    
    return new ResponseEntity<>(organizationService.deleteOrganization(organization), HttpStatus.OK);
  }
  

  @GetMapping(path = {
      "/organizations/mapping/{organizationLabel}",  // deprecated
      "/network-groups/mapping/{organizationLabel}"
  })
  public ResponseEntity<NetworkGroupResult> getNetworkGroup(
      @PathVariable("organizationLabel") String organizationLabel
      ) {
    Organization organization = organizationService.getOrganization(organizationLabel).orElse(null);
    if (organization == null) {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    
    NetworkGroupResult networkGroupResult = 
        networkGroupService.getNetworkGroupByOrganizationLabel(organizationLabel);
    if (networkGroupResult == null) {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    return new ResponseEntity<>(networkGroupResult, HttpStatus.OK);
  }
  
  
  
  // deprecated endpoint for adding an idp (before IdPs became their own resource)

  @Value
  public static class AddIdentityProviderCommand {
    @ValidAs(field = "auth0ConnectionId", on = IdentityProvider.class)
    private final String auth0ConnectionId;
    @ValidAs(field = "name", on = IdentityProvider.class)
    private final String identityProviderName;
    @ValidAs(field = "auth0ConnectionType", on = IdentityProvider.class)
    private final Auth0ConnectionType auth0ConnectionType;
  }
  
  @Deprecated
  @PostMapping(path = {
      "/tenants/{id}/identity-providers",  // deprecated
      "/organizations/{id}/identity-providers"
  })
  public ResponseEntity<Organization> addIdentityProvider(
      @AuthAction(action="create-identity-provider", on=Organization.class) @PathVariable("id") UUID id,
      @Valid @RequestBody AddIdentityProviderCommand command
      ) {
    Organization organization = organizationService.getOrganization(id)
        .orElseThrow(() -> new IllegalArgumentException("No such organization by that id"));
    
    identityProviderService.create(
        organization.getId(),
        command.getAuth0ConnectionId(),
        command.getIdentityProviderName(),
        command.getAuth0ConnectionType());
    
    // update the organization now that we added an IdP to it...
    organization = organizationService.getOrganization(id).orElse(null);
    
    return new ResponseEntity<>(organization, HttpStatus.CREATED);
  }

  @Value
  public static class AddMappingCommand {
    @NotNull
    @Size(max = 8)
    private final String networkGroupShortName;
  }

  @PostMapping(path = "/organizations/{id}/mapping")
  public ResponseEntity<Void> addNetworkGroupMapping(
      @AuthAction(action="create-group-mapping", on=Organization.class) @PathVariable("id") UUID id,
      @Valid @RequestBody AddMappingCommand addMappingCommand
  ) {
    Organization organization = organizationService.getOrganization(id)
        .orElseThrow(() -> new IllegalArgumentException("No such organization by that id"));

    if (organization.isDeleted()) {
      throw new IllegalStateException("Organization is deleted");
    }

    // Validate networkGroupShortName?  Identity uses data currently for NG, which is deprecated
    networkGroupService.mapNetworkGroupShortNameToOrganizationLabel(addMappingCommand.getNetworkGroupShortName(), organization.getLabel());

    return new ResponseEntity<>(HttpStatus.OK);
  }
  

  private PagedModel<EntityModel<Organization>> relink(Page<Organization> organization) {
    return HalUtil.relink(organization);
  }
}
